美文网首页
JMM + volatile+CAS+ABA

JMM + volatile+CAS+ABA

作者: Minority | 来源:发表于2020-03-06 13:33 被阅读0次

    文章讲解流程

    JMM ==> volatile 解决可见性 ==>
    AtomicInteger解决原子性 ==> CAS (compareAndSwap) ==>
    Unsafe ==> CAS的底层思想(compareAndSwap) ==>
    ABA问题 ==> 原子引用更新(AtomicReference) ==>
    如何规避ABA问题(atomicStampedReference) ==>END

    转载自:https://github.com/MaJesTySA/JVM-JUC-Core/blob/master/docs/JUC.md#jmm

    JMM

    JMM是指Java内存模型,不是Java内存布局,不是所谓的栈、堆、方法区。

    每个Java线程都有自己的工作内存。操作数据,首先从主内存中读,得到一份拷贝,操作完毕后再写回到主内存。

    JMM可能带来可见性原子性有序性问题。所谓可见性,就是某个线程对主内存内容的更改,应该立刻通知到其它线程。原子性是指一个操作是不可分割的,不能执行到一半,就不执行了。所谓有序性,就是指令是有序的,不会被重排。

    volatile关键字

    volatile关键字是Java提供的一种轻量级同步机制。它能够保证可见性有序性,但是不能保证原子性

    可见性

    可见性测试

    class MyData{
        int number=0;
        //volatile int number=0;
    
        AtomicInteger atomicInteger=new AtomicInteger();
        public void setTo60(){
            this.number=60;
        }
    
        //此时number前面已经加了volatile,但是不保证原子性
        public void addPlusPlus(){
            number++;
        }
    
        public void addAtomic(){
            atomicInteger.getAndIncrement();
        }
    }
    
    //volatile可以保证可见性,及时通知其它线程主物理内存的值已被修改
    private static void volatileVisibilityDemo() {
        System.out.println("可见性测试");
        MyData myData=new MyData();//资源类
        //启动一个线程操作共享数据
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t come in");
            try {TimeUnit.SECONDS.sleep(3);myData.setTo60();
            System.out.println(Thread.currentThread().getName()+"\t update number value: "+myData.number);}catch (InterruptedException e){e.printStackTrace();}
        },"AAA").start();
        while (myData.number==0){
         //main线程持有共享数据的拷贝,一直为0
        }
        System.out.println(Thread.currentThread().getName()+"\t mission is over. main get number value: "+myData.number);
    }
    

    MyData类是资源类,一开始number变量没有用volatile修饰,所以程序运行的结果是:

    可见性测试
    AAA  come in
    AAA  update number value: 60
    

    虽然一个线程把number修改成了60,但是main线程持有的仍然是最开始的0,所以一直循环,程序不会结束。

    如果对number添加了volatile修饰,运行结果是:

    AAA  come in
    AAA  update number value: 60
    main     mission is over. main get number value: 60
    

    可见某个线程对number的修改,会立刻反映到主内存上。

    原子性

    volatile并不能保证操作的原子性。这是因为,比如一条number++的操作,会形成3条指令。

    getfield        //读
    iconst_1    //++常量1
    iadd        //加操作
    putfield    //写操作
    

    假设有3个线程,分别执行number++,都先从主内存中拿到最开始的值,number=0,然后三个线程分别进行操作。假设线程0执行完毕,number=1,也立刻通知到了其它线程,但是此时线程1、2已经拿到了number=0,所以结果就是写覆盖,线程1、2将number变成1。

    解决的方式就是:

    1. addPlusPlus()方法加锁。
    2. 使用java.util.concurrent.AtomicInteger类。
    private static void atomicDemo() {
        System.out.println("原子性测试");
        MyData myData=new MyData();
        for (int i = 1; i <= 20; i++) {
            new Thread(()->{
                for (int j = 0; j <1000 ; j++) {
                    myData.addPlusPlus();
                    myData.addAtomic();
                }
            },String.valueOf(i)).start();
        }
        while (Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+"\t int type finally number value: "+myData.number);
        System.out.println(Thread.currentThread().getName()+"\t AtomicInteger type finally number value: "+myData.atomicInteger);
    }
    

    结果:可见,由于volatile不能保证原子性,出现了线程重复写的问题,最终结果比20000小。而AtomicInteger可以保证原子性。

    原子性测试
    main     int type finally number value: 17542
    main     AtomicInteger type finally number value: 20000
    

    有序性

    有序性案例

    volatile可以保证有序性,也就是防止指令重排序。所谓指令重排序,就是出于优化考虑,CPU执行指令的顺序跟程序员自己编写的顺序不一致。就好比一份试卷,题号是老师规定的,是程序员规定的,但是考生(CPU)可以先做选择,也可以先做填空。

    int x = 11; //语句1
    int y = 12; //语句2
    x = x + 5;  //语句3
    y = x * x;  //语句4
    

    以上例子,可能出现的执行顺序有1234、2134、1342,这三个都没有问题,最终结果都是x = 16,y=256。但是如果是4开头,就有问题了,y=0。这个时候就不需要指令重排序。

    volatile底层是用CPU的内存屏障(Memory Barrier)指令来实现的,有两个作用,一个是保证特定操作的顺序性,二是保证变量的可见性。在指令之间插入一条Memory Barrier指令,告诉编译器和CPU,在Memory Barrier指令之间的指令不能被重排序。

    哪些地方用到过volatile?

    单例模式的安全问题

    常见的DCL(Double Check Lock)模式虽然加了同步,但是在多线程下依然会有线程安全问题。

    public class SingletonDemo {
        private static SingletonDemo singletonDemo=null;
        private SingletonDemo(){
            System.out.println(Thread.currentThread().getName()+"\t 我是构造方法");
        }
        //DCL模式 Double Check Lock 双端检索机制:在加锁前后都进行判断
        public static SingletonDemo getInstance(){
            if (singletonDemo==null){
                synchronized (SingletonDemo.class){
                     if (singletonDemo==null){
                         singletonDemo=new SingletonDemo();
                     }
                }
            }
            return singletonDemo;
        }
    
        public static void main(String[] args) {
            for (int i = 0; i < 10; i++) {
                new Thread(()->{
                    SingletonDemo.getInstance();
                },String.valueOf(i+1)).start();
            }
        }
    }
    

    这个漏洞比较tricky,很难捕捉,但是是存在的。instance=new SingletonDemo();可以大致分为三步

    memory = allocate();     //1.分配内存
    instance(memory);    //2.初始化对象
    instance = memory;   //3.设置引用地址
    

    其中2、3没有数据依赖关系,可能发生重排。如果发生,此时内存已经分配,那么instance=memory不为null。如果此时线程挂起,instance(memory)还未执行,对象还未初始化。由于instance!=null,所以两次判断都跳过,最后返回的instance没有任何内容,还没初始化。

    解决的方法就是对singletondemo对象添加上volatile关键字,禁止指令重排。

    CAS

    CAS是指Compare And Swap比较并交换,是一种很重要的同步思想。如果主内存的值跟期望值一样,那么就进行修改,否则一直重试,直到一致为止。

    public class CASDemo {
        public static void main(String[] args) {
            AtomicInteger atomicInteger=new AtomicInteger(5);
            System.out.println(atomicInteger.compareAndSet(5, 2019)+"\t current data : "+ atomicInteger.get());
            //修改失败
            System.out.println(atomicInteger.compareAndSet(5, 1024)+"\t current data : "+ atomicInteger.get());
        }
    }
    

    第一次修改,期望值为5,主内存也为5,修改成功,为2019。第二次修改,期望值为5,主内存为2019,修改失败。

    查看AtomicInteger.getAndIncrement()方法,发现其没有加synchronized也实现了同步。这是为什么?

    CAS底层原理

    AtomicInteger内部维护了volatile int valueprivate static final Unsafe unsafe两个比较重要的参数。

    public final int getAndIncrement(){
        return unsafe.getAndAddInt(this,valueOffset,1);
    }
    

    AtomicInteger.getAndIncrement()调用了Unsafe.getAndAddInt()方法。Unsafe类的大部分方法都是native的,用来像C语言一样从底层操作内存。

    public final int getAnddAddInt(Object var1,long var2,int var4){
        int var5;
        do{
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
        return var5;
    }
    

    这个方法的var1和var2,就是根据对象偏移量得到在主内存的快照值var5。然后compareAndSwapInt方法通过var1和var2得到当前主内存的实际值。如果这个实际值快照值相等,那么就更新主内存的值为var5+var4。如果不等,那么就一直循环,一直获取快照,一直对比,直到实际值和快照值相等为止。

    比如有A、B两个线程,一开始都从主内存中拷贝了原值为3,A线程执行到var5=this.getIntVolatile,即var5=3。此时A线程挂起,B修改原值为4,B线程执行完毕,由于加了volatile,所以这个修改是立即可见的。A线程被唤醒,执行this.compareAndSwapInt()方法,发现这个时候主内存的值不等于快照值3,所以继续循环,重新从主内存获取。

    CAS缺点

    CAS实际上是一种自旋锁:

    1. 一直循环,开销比较大。
    2. 只能保证一个变量的原子操作,多个变量依然要加锁。
    3. 引出了ABA问题

    ABA问题

    所谓ABA问题,就是比较并交换的循环,存在一个时间差,而这个时间差可能带来意想不到的问题。比如线程T1将值从A改为B,然后又从B改为A。线程T2看到的就是A,但是却不知道这个A发生了更改。尽管线程T2 CAS操作成功,但不代表就没有问题。 有的需求,比如CAS,只注重头和尾,只要首尾一致就接受。但是有的需求,还看重过程,中间不能发生任何修改,这就引出了AtomicReference原子引用。

    AtomicReference

    AtomicInteger对整数进行原子操作,如果是一个POJO呢?可以用AtomicReference来包装这个POJO,使其操作原子化。

    User user1 = new User("Jack",25);
    User user2 = new User("Lucy",21);
    AtomicReference<User> atomicReference = new AtomicReference<>();
    atomicReference.set(user1);
    System.out.println(atomicReference.compareAndSet(user1,user2)); // true
    System.out.println(atomicReference.compareAndSet(user1,user2)); //false
    

    AtomicStampedReference和ABA问题的解决

    使用AtomicStampedReference类可以解决ABA问题。这个类维护了一个“版本号”Stamp,在进行CAS操作的时候,不仅要比较当前值,还要比较版本号。只有两者都相等,才执行更新操作。

    AtomicStampedReference.compareAndSet参数:
    AtomicStampedReference.compareAndSet(expectedReference,newReference,oldStamp,newStamp);

    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.atomic.AtomicReference;
    import java.util.concurrent.atomic.AtomicStampedReference;
    
    public class ABADemo {
        static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
        static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
    
        public static void main(String[] args) {
            System.out.println("======ABA问题的产生======");
    
            new Thread(() -> {
                atomicReference.compareAndSet(100, 101);
                atomicReference.compareAndSet(101, 100);
            }, "t1").start();
    
            new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
    
                }
                System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get().toString());
            }, "t2").start();
    
            try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
    
            System.out.println("======ABA问题的解决======");
            new Thread(() -> {
                int stamp = atomicStampedReference.getStamp();
                System.out.println(Thread.currentThread().getName() + "\t第一次版本号: " + stamp);
                try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
                atomicStampedReference.compareAndSet(100,101,
                        atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
                System.out.println(Thread.currentThread().getName() + "\t第二次版本号: " + atomicStampedReference.getStamp());
                atomicStampedReference.compareAndSet(101,100,
                        atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
                System.out.println(Thread.currentThread().getName() + "\t第三次版本号: " + atomicStampedReference.getStamp());
            }, "t3").start();
    
            new Thread(() -> {
                int stamp = atomicStampedReference.getStamp();
                System.out.println(Thread.currentThread().getName() + "\t第一次版本号: " + stamp);
                try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
                boolean result=atomicStampedReference.compareAndSet(100,2019,
                        stamp,stamp+1);
                System.out.println(Thread.currentThread().getName()+"\t修改成功与否:"+result+"  当前最新版本号"+atomicStampedReference.getStamp());
                System.out.println(Thread.currentThread().getName()+"\t当前实际值:"+atomicStampedReference.getReference());
            }, "t4").start();
        }
    }
    
    /**Output:
     * ======ABA问题的产生======
     * true 2019
     * ======ABA问题的解决======
     * t3   第一次版本号: 1
     * t4   第一次版本号: 1
     * t3   第二次版本号: 2
     * t3   第三次版本号: 3
     * t4   修改成功与否:false  当前最新版本号3
     * t4   当前实际值:100
     */
    

    相关文章

      网友评论

          本文标题:JMM + volatile+CAS+ABA

          本文链接:https://www.haomeiwen.com/subject/wzobrhtx.html