码迷,mamicode.com
首页 > 其他好文 > 详细

volatile | CAS| ABA

时间:2019-05-07 18:14:57      阅读:141      评论:0      收藏:0      [点我收藏+]

标签:表示   nts   round   rgs   line   排序   乱序   引用   zed   

 

JMM

JMM(Java内存模型Java Memory Model)是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式。

JMM关于同步的规定:

  ①线程解锁前,必须把共享变量的值刷新回主内存;

  ②线程解锁前,必须读取主内存的最新值到自己的工作内存;

  ③加锁解锁是同一把锁;

技术图片

技术图片

 

①把变量读到各个线程的工作内存中;②线程运算完之后把变量的值改好,然后把它写回主内存;③可见性--让其他线程马上知道这个值;

各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的;

这就可能存在一个线程AAA修改了共享变量X的值但还未写回主内存时,另外一个线程BBB又对主内存中同一个共享变量X进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题;

 可见性

class MyData{
    volatile int num = 0;
    public void add(){
        this.num = 60;
    }
}
public class TestJMM {
    public static void main(String[] args) {
        MyData myData = new MyData();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\tcome in");
            //暂停一会线程
            try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) { e.printStackTrace();}
            myData.add();
            System.out.println(Thread.currentThread().getName() + "\tupdated number value:" + myData.num);

            },"aa").start();
            //第二个线程就是main线程
        while (myData.num == 0){
            //main一直在循环,等待num的值不为0
        }
        System.out.println(Thread.currentThread().getName() + "\tmission is over, main get num value:" + myData.num);

    }
}
//volatile可以保证可见性,及时通知其他线程,主物理内存的值已经被修改;如果不加volatile则主线程就一直在那等待着,程序就卡那了;
 aa come in
aa updated number value:60
main mission is over, main get num value:60
 

 不保证原子性

class MyData{
    volatile int num = 0;
    public void add(){
        this.num = 60;
    }
    // num前面加了volatile关键字,不保证原子性
    public void addPlusPlus(){
        num++;
    }
}
public class TestJMM {
    public static void main(String[] args) {
        MyData myData = new MyData();

        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++) {
                    myData.addPlusPlus(); //最后结果应该是20000
                }
                }, String.valueOf(i)).start();
        }
        //等待上面20个线程计算完之后,再用main线程取得最后结果
        while (Thread.activeCount() > 2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + "\t finally num is:" + myData.num);
    }
}

main     finally num is:19951 结果是随机的;

 n++的字节码过程

public class T1 {
    volatile int n = 0;
    public void add(){
        n++; //n++被拆分成3个指令:①执行getfield拿到原始值n;②执行iadd进行加1操作;③执行putfield把累加后的值写回;
    }
}
  public void add();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field n:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field n:I
        10: return
      LineNumberTable:
        line 6: 0
        line 7: 10
      LocalVariableTable:

不保证原子性的解决方法:

class MyData{
     volatile int num = 0;
    public void add(){
        this.num = 60;
    }
    // num前面加了volatile关键字,不保证原子性
    public void  addPlusPlus(){
        num++;
    }

    AtomicInteger atomicInteger = new AtomicInteger();
    public void addMyAutomic(){
        atomicInteger.getAndIncrement();   //如何解决原子性,加sync...,它锁的范围太广了;使用JUC下的AtomicInteger
    }
}
public class TestJMM {
    public static void main(String[] args) {
        MyData myData = new MyData();
        //TODO 原子性的解决方法
        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++) {
                    myData.addPlusPlus(); //最后结果应该是20000
                    myData.addMyAutomic();
                }
                }, String.valueOf(i)).start();
        }
        //等待上面20个线程计算完之后,再用main线程取得最后结果
        while (Thread.activeCount() > 2){ //如果线程活跃个数>2,就暂停当前线程,把CPU资源让出
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + "\t finally num is:" + myData.num);
        System.out.println(Thread.currentThread().getName() + "\t AtomicInteger type,finally num is:" + myData.atomicInteger);

    }
}

main finally num is:19356
main AtomicInteger type,finally num is:20000

指令做重排

技术图片

 技术图片

public class SingletonDemo {
    private static volatile SingletonDemo instance = null;
    //构造方法,都会执行main    我是构造方法SingletonDemo
    private SingletonDemo(){
        System.out.println(Thread.currentThread().getName() + "\t我是构造方法SingletonDemo");
    }
    //单例模式,在高并发下,还是会出异常,因为存在指令重排  DCL(Double Check Lock)双端检索机制,加锁前后都做判断 
    public static  SingletonDemo getInstance(){
/*        if (instance == null){
             instance = new SingletonDemo(); //单例模式加synchronized太重,需要控制的只是这一行
        }
        return instance;
        //在上边代码会出现以下情况:
        2    我是构造方法SingletonDemo
        4    我是构造方法SingletonDemo
        3    我是构造方法SingletonDemo
        1    我是构造方法SingletonDemo*/

        if (instance == null){
            synchronized (SingletonDemo.class){
                if (instance == null){
                    instance = new SingletonDemo();
                }
            }
        }
        return instance; //这里只是拿到了内存地址,数据还没拿到,还是有可能发生指定重排的;加volatile可实现禁止指令重排
    }
    public static void main(String[] args) {
        //单线程下
/*        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); //true
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); //true
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); //true*/

        //并发环境下,情况发生了很大变化
        for (int i = 1; i <= 10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }
}
--------------------------
1 我是构造方法SingletonDemo

 

volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象;

概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它有两个作用:

  ①保证特定操作的执行顺序;

  ②保证某些变量的内存可见性(利用该特性实现volatile的内存可见性);

由于编译器和处理器都能执行指令重排。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各自CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

技术图片

为了保证有序,需要禁止指定重排;

线程安全性获得保证:

工作内存与主内存同步延迟现象导致的可见性问题,可以使用synchronized或volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。

对于执行重排导致的可见性问题和有序性问题,可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化。

 

volatile只能保证其中的两个,不保证原子性;所以是低配版的轻量级;

你在哪些地方用过volatile? ①单例模式DCL代码

单例模式一共6种;懒汉、饿汉

 并发

 DCL(双端检锁)机制不一定线程安全,原因是有指令重排的存在,加入volatile可以禁止指令重排;

原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化

instance = new SingletonDemo();可以分为以下3步完成(伪代码)

memory = allocate();//1.分配对象内存空间

instance(memory);//2.初始化对象

instance = memory;//3.设置instance指向刚分配的内存地址,此时instance! = null

步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是运行的。

memory = allocate();//1.分配对象内存空间

instance = memory;//3.设置instance指向刚分配的内存地址,此时instance! = null,但是对象还没有初始化完成!

instance(memory);//2.初始化对象

但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。

所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。

 

单例模式DCL代码;单例模式volatile分析

  1Unsafe

  是CAS的核心类,由于java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。

Unsafe类存在于sun.misc包中,其中内部方法操作可以像C的指针一样直接操作内存,因为java中CAS操作的执行依赖于Unsafe类的方法。

注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。

2 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。

技术图片

3.

它时来判断内存中的某个值是否为期望值;

技术图片

 

 技术图片

 

 技术图片

CAS缺点:

技术图片

CAS没有加锁,要保证数据一致性、原子性,使用do while

技术图片

引导出ABA

 Unsafe类和CAS思想(自旋)

AtomicInteger--->

技术图片

ABA狸猫换太子

 技术图片

ABA,只管结果不管过程,只要头跟尾对上就可以了; 

解决ABA问题?

AtomicReference原子引用
@Getter
@ToString
@AllArgsConstructor
class User{
    String username;
    int age;

}
public class AtomicReferenceDemo {
    public static void main(String[] args) {
        User kris = new User("kris", 22);
        User alex = new User("alex", 23);
        AtomicReference<User> atomicReference = new AtomicReference<>();
        atomicReference.set(kris); //如果想对某个类进行原子性包装
        System.out.println(atomicReference.compareAndSet(kris, alex) + "\t" + atomicReference.get().toString());
        System.out.println(atomicReference.compareAndSet(kris, alex) + "\t" + atomicReference.get().toString());

/*      true    User(username=alex, age=23)
        false    User(username=alex, age=23)

        */
    }
}

 

时间戳的原子引用  就是解决ABA问题

技术图片

ABA问题的解决:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABADemo { //ABA问题的解决 AtomicStampedReference
    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, 101);
            },"t1").start();
        new Thread(() -> {
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(atomicReference.compareAndSet(101, 2019) + "\t" + atomicReference.get());

            },"t2").start(); //true     2019  成功改为了2019,但是中间是有人动过的;
        //暂停一会线程
        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第1次版本号:" + stamp);//1
            //暂停1s t3线程
            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第2次版本号:" + atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName() + "\t第3次版本号:" + atomicStampedReference.getStamp());},
                "t3").start();

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第1次版本号:" + stamp);//1
            //暂停3s 4线程保证上面的t3线程完成了一次ABA操作t3 t4都暂停下就是让它们的起点一样,都拿到版本号为1的数据 100 
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
            boolean restlt = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1); //t4提交的版本号2,但服务器上最新的版本号是3;
            System.out.println(Thread.currentThread().getName() + "\t修改成功否:" + restlt + "\t当前最新版本号:" + atomicStampedReference.getStamp());
            System.out.println(Thread.currentThread().getName() + "\t当前实际最新值:" + atomicStampedReference.getReference());
            },"t4").start();
    }
}

============ABA问题的产生============
true    2019
==================ABA问题的解决#############
t3    第1次版本号:1
t4    第1次版本号:1
t3    第2次版本号:2
t3    第3次版本号:3
t4    修改成功否:false    当前最新版本号:3
t4    当前实际最新值:100

 

volatile | CAS| ABA

标签:表示   nts   round   rgs   line   排序   乱序   引用   zed   

原文地址:https://www.cnblogs.com/shengyang17/p/10765881.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!