前言
volatile关键字的作用有两个:
- 线程可见性:一个线程修改一个共享变量时,另一个线程能读到这个修改的值
- 顺序一致性:禁止指令重排
但是volatile无原子性。原子操作,如:i=1; 但是像j=i(2步)或i++(3步)不是原子操作。 j=i:
- 取i值
- i值赋给j
i++:
- 取i值
- i值加1
- 新值写入缓存i
验证volatile不具备原子性
public class VolatileAtomDemo {
volatile int number=0;
public void addPlusPlus(){
number++;
}
/**
* 打印当前所有线程信息
*/
public void printlnCurrentAllThread(){
// 获取当前所有线程
Thread[] threads = new Thread[Thread.activeCount()];
int actualSize = Thread.enumerate(threads);
// 打印线程信息
for (int i = 0; i < actualSize; i++) {
Thread thread = threads[i];
System.out.println("Thread " + i + ": " + thread.getName());
}
}
}
fun main() {
println("parent name:${Thread.currentThread().name}")
val volatileAtomDemo = VolatileAtomDemo()
for (i in 0..199) {//200
Thread(object : Runnable {
override fun run() {
for (j in 0..999) {//1000
volatileAtomDemo.addPlusPlus()
}
}
}, "test" + i).start()
}
// 我们的累加线程是否得到充分执行(test$i 如果还未完全执行则暂停当前线程结束,等待我们的累加线程执行完毕), 后台默认线程除外
while (Thread.activeCount() > 4) {
// 当前线程暂停执行,以便其他线程有机会执行
Thread.yield()
}
// 如果volatile保证原子性,则number结果应该是200000
println("threadName:${Thread.currentThread().name} number:${volatileAtomDemo.number}")
}
上面多次测试输出结果小于200000。则volatile修饰的变量不具备原子性。 解决volatile原子不一致性的办法:
- 在方法前加synchronized解决
volatile int number=0;
public synchronized void addPlusPlus(){
number++;
}
- 加锁解决:
volatile int number=0;
Lock lock = new ReentrantLock();
public void addPlusPlus(){
lock.lock();
number++;
lock.unlock();
}
- 原子类解决:
AtomicInteger number=new AtomicInteger();
public void addPlusPlus(){
number.getAndIncrement();
}
原子类用在多线程环境中对共享变量进行原子操作。提供:自增、自减、比较并交换等。
- 线程安全
- 原子性操作
- 性能优化
volatile使用场景
单例模式的DCL双端检锁机制
public class Singleton {
//volatile保证了不论多线程怎样写,读到的都是最新的值,保证本地内存的一致性,都与主内存相同
// 此处使用valotile起到禁止指令重排的作用,可以保证DCL线程安全。
private volatile static Singleton uniqueInstance;
private Singleton(){}
// DCL(Double Check Lock 双端检锁机制)
// DCL机制能保证多线程环境下99.99%情况线程安全,但是还存在0.01%线程不安全
// 所以需要使用volatile禁止指令重排来保证DCL多线程环境下绝对安全
public static Singleton getInstance(){
//检查实例,如果不存在,则进入同步区,只有第一次才会执行此处代码
if(uniqueInstance==null){
synchronized (Singleton.class){
//再检查一次实例是否为空
if(uniqueInstance==null){
uniqueInstance=new Singleton();
}
}
}
return uniqueInstance;
}
}
CAS
CAS底层核心:Unsafe类 atomicInteger.getAndIncrement();java无法访问底层系统,要通过本地(native)方法访问,Unsafe(sun.misc)是一个后门,直接操作特定内存的数据。像C指针直接操作内存,java的CAS操作依赖Unsafe方法。UnSafe类所有方法是native修饰。
@ForceInline
private static final jdk.internal.misc.Unsafe theInternalUnsafe;
public final int getAndAddInt(Object o, long offset, int delta) {
return theInternalUnsafe.getAndAddInt(o, offset, delta);
}
CAS(Compare-And-Swap)是CPU并发原语。判断内存某个位置是否为预期值,如果是则更改为新值,这个过程是原子的。CAS体现在JAVA中是sun.misc.Unsafe类各个方法。调用Unsafe中的CAS方法,JVM实现出CAS汇编指令。完全依赖硬件的功能,原语的执行必须是连续的,执行过程不允许被中断,CAS是CPU的原子指令,不会造成数据不一致问题。
CAS底层是汇编
Unsafe类中compareAndSwapInt是一个本地方法,位于unsafe.cpp中
CAS缺点
循环时间长CPU开销大,只保证一个共享变量的原子操作,会引发ABA问题。 CAS操作:首先读取原始数据后,进行CAS操作前这个间隙会引发ABA问题 线程t1:读取x原始数据A,挂起。 线程t2:x改为B,然后又x改为A。 线程t1恢复,通过CAS比较,比较结果无变化。在这个间隙中可能会带来问题。
- 如果只关心结果,则ABA不是问题
- 如果关心过程,则ABA是个严重问题
- 如果用的是引用类型,引用不变,但是引用的值被改变了。那也是有问题的。
解决ABA问题:带版本号的原子引用(Java中使用版本戳AtomicMarkableReference 和 AtomicStampedReference)。
Java中提供了可以修改多个变量的原子操作AtomicReference将需要修改的包装成一个对象,然后用AtomicReference的compareAndSet方法进行替换即可:
fun test(){
val user = User("george", 30)
val atomicReference = AtomicReference(user)
Log.d(TAG,"name:${atomicReference.get().name} age:${atomicReference.get().age} user:$user")
atomicReference.compareAndSet(user,User("georegeRen",18))
Log.d(TAG,"name:${atomicReference.get().name} age:${atomicReference.get().age} user:$user")
}
输出:
name:george age:30 user:com.georege.leetcode2.MainActivity$User@bb8f8ec
name:georegeRen age:18 user:com.georege.leetcode2.MainActivity$User@bb8f8ec
可以看到AtomicReference的compareAndSet只改变了引用对象的属性值,对user应用地址前后是不变的。