volatile不保证原子性

180 阅读4分钟

前言

volatile关键字的作用有两个:

  1. 线程可见性:一个线程修改一个共享变量时,另一个线程能读到这个修改的值
  2. 顺序一致性:禁止指令重排

但是volatile无原子性。原子操作,如:i=1; 但是像j=i(2步)或i++(3步)不是原子操作。 j=i:

  1. 取i值
  2. i值赋给j

i++:

  1. 取i值
  2. i值加1
  3. 新值写入缓存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原子不一致性的办法:

  1. 在方法前加synchronized解决
volatile int number=0;
public synchronized void addPlusPlus(){
    number++;
}
  1. 加锁解决:
volatile int number=0;
Lock lock = new ReentrantLock();
public void addPlusPlus(){
   lock.lock();
   number++;
   lock.unlock();
}
  1. 原子类解决:
AtomicInteger number=new AtomicInteger();
public void addPlusPlus(){
    number.getAndIncrement();
}

原子类用在多线程环境中对共享变量进行原子操作。提供:自增、自减、比较并交换等。

  1. 线程安全
  2. 原子性操作
  3. 性能优化

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比较,比较结果无变化。在这个间隙中可能会带来问题。

  1. 如果只关心结果,则ABA不是问题
  2. 如果关心过程,则ABA是个严重问题
  3. 如果用的是引用类型,引用不变,但是引用的值被改变了。那也是有问题的。

解决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应用地址前后是不变的。