volatile与CAS
volatile
实际写程序,volatile 基本用不着,就是用来干面试的。
两个特性
- 保证线城可见性
java里面是堆内存的,堆内存是所有线程共享的内存。除了共享内存之外,每个线程都有自己的工作区域,叫做专属内存。如果在共享内存里有一个值的话,如果有两个线程同时访问这个值,他们会把它copy一份到自己的工作空间。然后如果对这个值进行改变,首先会在自己的工作里面改变,然后等到某一个CPU会把这个值写回到共享内存。但是什么时候写回,不好控制。此时如果另外一个线程直接读取它工作空间内的已经过时的值,显然就有问题了。此时缺少一个线程间的通知机制,叫做线程不可见。加了volatile之后就能保证一个线程改变了这个值,另一个线程马上就能看到。
volatile能保证线程可见性,底层用到了CPU的缓存一致性协议MESI。 - 禁止指令重排序
多核CPU为了提高计算效率,会把指令并行执行,称之为流水线式的执行。如果想要充分的利用这一点,就要求编译器把源码编译成指令之后呢,可能进行一个重新排序。逻辑上比如a=3,b=5,有可能被重排序为b=5,a=3;细节上就是汇编的指令重排序。(禁止指令重排序有什么好处呢?)
底层实现(阿里面试题)
JVM 层面的实现
- volatile 写操作S:
如果上面有写操作S,肯定不能和当前写操作S互换
如果下面有读操作L,肯定也不能和当前写操作S交换 - volatile 读操作L:
如果想面有读操作L,肯定不能和当前读操作L互换
如果下面有读操作S,肯定也不能和当前写操作L交换
操作系统层级的实现
无非两种:
- CPU原语:lfence、sfence、mfence
- 总线锁 lock 汇编指令
具体怎么实现由JVM的实现决定
volatile 在 HotSpot 里采用的 lock 指令。
volatile 修饰对象,怎么加屏障
对象的整个内存区域都会加上屏障,对对象的所有改动包括对它属性的改动,都是收屏障保护的。
DCL单例+volatile
禁止指令重排序有什么好处呢?通常会举这个例子。
1、单例模式
-
饿汉式(推荐使用,如果没有内存容量的限制,利用类加载器创建单例,这种做法线程安全)
public class Singleton { private static Singleton instance = new Singleton(); private Singleton() {} public Singlenton getInstance() { return instance; } }
-
懒汉式(最简模式)
public class Singleton { private static Singleton instance; private Singleton() {} public synchronized Singlenton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
2、DCL单例
DCL(double check lock)单例属于懒汉式的终极模式,可防止多线程并发访问时造成重复创建对象
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public Singlenton getInstance() {
if (instance == null) {
synchronized(Singlenton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
3、对象的创建过程(汇编创建过程)
- 1、为对象申请内存(半初始化)
- 2、对象的成员变量进行初始化
- 3、把对象的赋值给变量指针
Object o = new Object();
编译成汇编指令:
new #2 <java/lang/Object <init>>
dup
invokespecial #1 <java/lang/Object <init>>
astore_1
return
(astore_0 指向 this)
在创建对象的过程中,如果发生指令重排序,有可能会发生第2、3步进行交换。这样如果当一个线程创建对象时,执行完第1步先执行第3步,此时另一个线程也来访问这个对象,发现已经有值,便直接返回并使用,此时由于前一个线程还没有执行第2步,对象没有进行初始化,导致第二个线程在用这个对象是,取到的对象成员变量为0或false或null,并没有被初始化。
如果加了volatile,那么对对象的指令重排序就不允许存在了。
4、总结
- volatite 可以禁止指令重排序,这是 synchronized 做不到的。synchronized 只能保证原子性。
- 读屏障和写屏障是防止指令重排序用的。
volatile vs synchronized
- volatile 并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说 volatile 不能替代 synchronized
运行下面的程序,并分析结果
package com.mashibing.juc.c_012_Volatile;
import java.util.ArrayList;
import java.util.List;
public class T04_VolatileNotSync {
volatile int count = 0;
void m() {
for(int i=0; i<10000; i++) count++;
}
public static void main(String[] args) {
T04_VolatileNotSync t = new T04_VolatileNotSync();
List<Thread> threads = new ArrayList<Thread>();
for(int i=0; i<10; i++) {
threads.add(new Thread(t::m, "thread-"+i));
}
threads.forEach((o)->o.start());
threads.forEach((o)->{
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t.count);
}
}
运行结果:小于100000
分析:虽然第一个线程count++变成了1,此时第二个线程读到的是1,第三个线程读到的也是1,但是此时第二和第三个线程都执行count++变成了2,此时第二个线程写回了一个2,第三个线程也写回了一个2,结果就是少加了一个1
- 加了 volatile 虽然保证了 count 的可见性,但 count++ 本身并不是一个原子性的操作。(问:count++不是一句话吗,为什么没有原子性?答:编译成汇编以后会变成好几条操作指令,类似new Object()也只是一条语句,但是变成汇编以后就有好几条操作指令)。
要解决这个问题,用synchronized就可以。
package com.mashibing.juc.c_012_Volatile;
import java.util.ArrayList;
import java.util.List;
public class T05_VolatileVsSync {
/*volatile*/ int count = 0;
synchronized void m() {
for (int i = 0; i < 10000; i++)
count++;
}
public static void main(String[] args) {
T05_VolatileVsSync t = new T05_VolatileVsSync();
List<Thread> threads = new ArrayList<Thread>();
for (int i = 0; i < 10; i++) {
threads.add(new Thread(t::m, "thread-" + i));
}
threads.forEach((o) -> o.start());
threads.forEach((o) -> {
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t.count);
}
}
synchronzed的优化
锁的细化(fine lock)
在锁竞争激烈的情况下,应该让锁尽量细化,这样可以让更多的线程不至于阻塞,比如DCL单例就比同步方法的懒汉式单例,更适合高并发情形。
锁的粗化(coars lock)
如果一个方法里头有好多好多的细锁,可以把它粗化为一个大锁,这样可以避免频繁的锁竞争。
锁定同一个对象时的注意点
- 锁定某对象o,如果o的属性发生改变,不影响锁的使用
- 但是如果o变成另外一个对象,则锁定的对象发生改变
- 应该避免将锁定对象的引用变成另外的对象,可以加 final
package com.mashibing.juc.c_017_MoreAboutSync;
import java.util.concurrent.TimeUnit;
public class SyncSameObject {
final Object o = new Object();
void m() {
synchronized(o) {
while(true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
}
public static void main(String[] args) {
SyncSameObject t = new SyncSameObject();
//启动第一个线程
new Thread(t::m, "t1").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
//创建第二个线程
Thread t2 = new Thread(t::m, "t2");
t.o = new Object(); //锁对象发生改变,所以t2线程得以执行,如果注释掉这句话,线程2将永远得不到执行机会
t2.start();
}
}
不要用String、Long等基本类型来作为锁
不要以字符串常量作为锁定对象,因为并不是因为语法上的问题,而是由于这些对象在堆内存的常量池内都是独一份,如果和某些第三方类库不小心使用了同一个对象,造成诡异的死锁,将是极难排查出问题的。
package com.mashibing.juc.c_017_MoreAboutSync;
public class DoNotLockString {
String s1 = "Hello";
String s2 = "Hello";
void m1() {
synchronized(s1) {
}
}
void m2() {
synchronized(s2) {
}
}
}
CAS
面试重灾区,号称“无锁优化 自旋 乐观锁”,理解含义就行。
- 比较+更新 整体是一个原子操作
- 由于某些特别常见的操作,老是来回加锁,所以干错Java提供了一些常见操作的类,这些类的内部就自动带了锁,但这些锁不是sync重量级锁,而是CAS,号称无锁。
- 在JUC的atomic包下,很多以Atomic开头的类,都是以CAS操作来保证线程安全的类。最常见的类就是AtomicInteger,用法如下:
package com.mashibing.juc.c_018_00_AtomicXXX;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class T01_AtomicInteger {
/*volatile*/ //int count1 = 0;
AtomicInteger count = new AtomicInteger(0);
/*synchronized*/ void m() {
for (int i = 0; i < 10000; i++)
//if count1.get() < 1000
count.incrementAndGet(); //count1++
}
public static void main(String[] args) {
T01_AtomicInteger t = new T01_AtomicInteger();
List<Thread> threads = new ArrayList<Thread>();
for (int i = 0; i < 10; i++) {
threads.add(new Thread(t::m, "thread-" + i));
}
threads.forEach((o) -> o.start());
threads.forEach((o) -> {
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t.count);
}
}
Compare And Set
- CAS操作可以想象成一个方法,如下:
cas(V, Expected, NewValue):参数1:V的当前值,参数2:V的期望值(旧值),参数3:V要改成的新值 - 下面的模型,请参考 VarHandler 的 compareAndSet() 操作
CAS(V, Expected, NewValue) {
if V == Expected: // 如果V是我期望的值E,则V做修改改成NewValue
V = NewValue
otherwise try again or fail // 否则说明有其他线程改了这个值了,那么当前线程就通过自旋再试一下,此时E为上一次的V
}
- CPU原语支持:所以在CAS操作期间,语句的执行不会被打断
- 期望值怎么得到,就是再CAS操作之前先读一下,如果再CAS操作期间V发生了改变,那么就在CAS进行自旋操作的时候重新读
expected = read V
newValue = expected +/-* dolta
CAS(V, expected, newValue)
ABA问题
在一个线程1对变量进行cas操作时的期望值是a,在它还没有操作之前另一个线程2将变量改为b又改回为a,然后线程1执行cas操作时虽然期望值还是a,但其实已经被改过。
解决办法:
- 一种情况,如果对于像int型变量,这种值的变化无所谓嘛,完全可以忽略
- 对于像指向对象的变量,可能会有问题(你的女朋友虽然和你复合,但是期间她可能经历了很多别的男人)
- 想彻底解决ABA问题,可以加版本号,每次操作,版本号加1
Unsafe类(= c/c++的指针)
CAS怎么做到可以不加锁进行操作的,就是用到类Unsafe这个类。这个类只需了解就行,这个类里面的方法非常非常多。
- 在JDK8以前除了反射可以用之外,是不能直接使用的。原因与ClassLoader是有关系的。
- JDK11以后,Unsafe可以通过getUnsafe() 来获得,是个单例(饿汉式)
- Unsafe是用来干嘛的呢?它可以直接操纵JVM的内存。
- 所有Atomic打头的类,都是通过Unsafe里的compareAndSwap()和compareAndSet()这一类的原子性操作来实现的
CAS(实现数量自增的几种方式以及比较)
为什么要用CAS操作,因为效率比sync同步锁效率高得多。怎么证明?
Atomic Vs Synchronized
举个例子,有多个线程对一个long型变量进行++操作,这在我们实际工作当中非常常见,比如秒杀,有几种方式:(它们之间的效率高低并不一定是确定的)
- sychronized
- AtomicLong
- LongAdder
package com.mashibing.juc.c_018_00_AtomicXXX;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
public class T02_AtomicVsSyncVsLongAdder {
static long count2 = 0L;
static AtomicLong count1 = new AtomicLong(0L);
static LongAdder count3 = new LongAdder();
public static void main(String[] args) throws Exception {
Thread[] threads = new Thread[1000];
for(int i=0; i<threads.length; i++) {
threads[i] =
new Thread(()-> {
for(int k=0; k<100000; k++) count1.incrementAndGet();
});
}
long start = System.currentTimeMillis();
for(Thread t : threads ) t.start();
for (Thread t : threads) t.join();
long end = System.currentTimeMillis();
//TimeUnit.SECONDS.sleep(10);
System.out.println("Atomic: " + count1.get() + " time " + (end-start));
//-----------------------------------------------------------
Object lock = new Object();
for(int i=0; i<threads.length; i++) {
threads[i] =
new Thread(new Runnable() {
@Override
public void run() {
for (int k = 0; k < 100000; k++)
synchronized (lock) {
count2++;
}
}
});
}
start = System.currentTimeMillis();
for(Thread t : threads ) t.start();
for (Thread t : threads) t.join();
end = System.currentTimeMillis();
System.out.println("Sync: " + count2 + " time " + (end-start));
//----------------------------------
for(int i=0; i<threads.length; i++) {
threads[i] =
new Thread(()-> {
for(int k=0; k<100000; k++) count3.increment();
});
}
start = System.currentTimeMillis();
for(Thread t : threads ) t.start();
for (Thread t : threads) t.join();
end = System.currentTimeMillis();
//TimeUnit.SECONDS.sleep(10);
System.out.println("LongAdder: " + count1.longValue() + " time " + (end-start));
}
static void microSleep(int m) {
try {
TimeUnit.MICROSECONDS.sleep(m);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
结果:
LongAdder > AmoticLong > sychronized
由此可见LongAdder效率最快,但是如果线程数量减少,LongAdder未必有优势,甚至 AmoticLong也未必比 sychronized快。为什么?
- AmoticLong为什么 sychronized快?因为 AmoticLong只需进行CAS操作,而 sychronized可能需要向操作系统取申请重型锁(锁升级的过程)。
- LongAdder为什么 AmoticLong快?因为LongAdder内部有一个分段锁的概念。
LongAdder
AmoticLong这个CAS操作有没有问题呢?肯定是有的。比如说大量的线程同时并发修改一个 AmoticLong,可能有很多线程会不停的自旋,进入一个无限重复的循环中。
- 分段锁 原理戳这里
Java 8推出了一个新的类,LongAdder,他就是尝试使用分段CAS以及自动分段迁移的方式来大幅度提升多线程高并发执行CAS操作的性能!