JUC面试题一

189 阅读25分钟

欢迎大家关注 github.com/hsfxuebao/j… ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈

1、volatile 关键字

谈谈你对volatile的理解

1.1、volatile 三大特性

volatile是java虚拟机提供的轻量级同步机制

可以将 volatile 看作是乞丐版的 synchronized 锁

  1. 保证内存可见性
  2. 禁止指令重排
  3. 不保证原子性

1.2、JMM 内存模型

1.2.1、谈谈 JMM

谈谈 JMM

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

JMM关于同步的规定:

  1. 线程解锁前,必须把共享变量的值刷新回主内存
  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存
  3. 加锁解锁是同一把锁

内存可见性

  1. 由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域
  2. Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行
  3. 一个线程如果想要修改主内存中的变量,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存
  4. 线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:

image-20200807104915773

1.2.2、内存可见性

JMM volatile 的内存可见性

  1. 通过前面对JMM的介绍,我们知道:各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的
  2. 这就可能存在一个线程AAA修改了共享变量X的值但还未写回主内存时,另外一个线程BBB又对主内存中同一个共享变量X进行操作
  3. 但此时A线程工作内存中的共享变量X对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题

代码示例:内存可见性

代码示例 1 :线程间内存不可见

  • 代码:number 变量未加 volatile 关键字

    public class VolatileDemo {

    public static void main(String[] args) {
        volatileVisibilityDemo();
    }
    
    /*
    验证volatile的可见性
        1.1 加入int number=0,number变量之前根本没有添加volatile关键字修饰,没有可见性
        1.2 添加了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);
    }
    

    }

    class MyData {

    int number = 0;
    
    public void setTo60() {
        this.number = 60;
    }
    

    } 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051

  • 程序运行结果:程序未能停下来

image-20200807112939904

  • 分析:
    • 在上述程序中,两个线程:main 线程和 AAA 线程,同时对 myData 数据进行操作
    • 由于 AAA 线程先睡眠了 3s ,所以 main 线程先拿到了 myData.number 的值,将该值拷贝回自己线程的工作内存,此时 myData.number = 0
    • AAA 线程 3s 后醒来,将 myData.number 拷贝回自己线程的工作内存,修改为 60 后,写回主内存
    • 但 AAA 线程将 myData.number 的值写回主内存后,并不会去通知 main 线程,所以 main 线程一直拿着自己线程的工作内存中的 myData.number = 0 ,搁那儿 while 循环呢

代码示例 2 :volatile 保证线程间内存的可见性

  • 代码:number 变量加上 volatile 关键字

    public class VolatileDemo {

    public static void main(String[] args) {
        volatileVisibilityDemo();
    }
    
    /*
    验证volatile的可见性
        1.1 加入int number=0,number变量之前根本没有添加volatile关键字修饰,没有可见性
        1.2 添加了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 线程收到通知后,会修改自己线程内存中的值
        }
        System.out.println(Thread.currentThread().getName() + "\t mission is over. main get number value: " + myData.number);
    }
    

    }

    class MyData {

    // volatile可以保证可见性,及时通知其它线程主物理内存的值已被修改
    volatile int number = 0;
    
    public void setTo60() {
        this.number = 60;
    }
    

    } 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051

  • 程序运行结果:停下来了哦

image-20200807114105344

  • 分析:由于有volatile 关键字的存在,当 AAA 线程修改了 myData.number 的值后,main 线程会受到通知,从而刷新自己线程工作内存中的值

1.2.3、原子性

原子性是什么?

原子性是不可分割,完整性。也即某个线程正在做某个具体业务时,中间不可以被加塞或者分割, 需要整体完成,要么同时成功,要么同时失败(类比数据库原子性)

代码示例:volatile 不保证原子性

  • 代码

    public class VolatileDemo {

    public static void main(String[] args) {
        atomicDemo();
    }
    
    /*
    2 验证volatile不保证原子性
        2.1 原子性是不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者分割。
            需要整体完成,要么同时成功,要么同时失败。
    
        2.2 volatile不可以保证原子性演示
    
        2.3 如何解决原子性
            1)加sync
            2)使用我们的JUC下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();
                }
            }, String.valueOf(i)).start();
        }
        /*
        需要等待上述20个线程都计算完成后,再用main线程去的最终的结果是多少?
        只要上述20个线程还有在执行的,main线程便礼让,让他们执行,直至最后只剩main线程
         */
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + "\t int type finally number value: " + myData.number);
    }
    

    }

    class MyData {

    // volatile可以保证可见性,及时通知其它线程主物理内存的值已被修改
    volatile int number = 0;
    
    public void setTo60() {
        this.number = 60;
    }
    
    //此时number前面已经加了volatile,但是不保证原子性
    public void addPlusPlus() {
        number++;
    }
    

    } 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061

  • 程序运行结果

    原子性测试 main int type finally number value: 19077 12

从字节码角度解释原子性

  • java 源代码

    public class T1 {

    volatile int n = 0;
    
    public void add() {
        n++;
    }
    

    } 12345678910111213141516

  • n++ 的字节码指令

    0 aload_0 1 dup 2 getfield #2 <com/Heygo/T1.n> 5 iconst_1 6 iadd 7 putfield #2 <com/Heygo/T1.n> 10 return 1234567

n++ 分为三步

  1. 第一步:执行 getfield 指令拿到主内存中 n 的值
  2. 第二步:执行 iadd 指令执行加 1 的操作(线程工作内存中的变量副本值加 1)
  3. 第三步:执行 putfield 指令将累加后的 n 值写回主内存

PS :iconst_1 是将常量 1 放入操作数栈中,准备执行 iadd 操作

分析多线程写值,值丢失的原因

  1. 两个线程:线程 A和线程 B ,同时拿到主内存中 n 的值,并且都执行了加 1 的操作
  2. 线程 A 先执行 putfield 指令将副本的值写回主内存,线程 B 在线程 A 之后也将副本的值写回主内存
  3. 此时,就会出现写覆盖、丢失写值的情况

解决原子性问题:

两个解决办法:

  1. 对 addPlusPlus() 方法加同步锁(加锁这个解决方法太重)
  2. 使用 Java.util.concurrent.AtomicInteger
  • 代码:使用 AtomicInteger 类保证 i++ 操作的原子性

    public class VolatileDemo {

    public static void main(String[] args) {
        atomicDemo();
    }
    
    /*
    2 验证volatile不保证原子性
        2.1 原子性是不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者分割。
            需要整体完成,要么同时成功,要么同时失败。
    
        2.2 volatile不可以保证原子性演示
    
        2.3 如何解决原子性
            1)加sync
            2)使用我们的JUC下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();
        }
        /*
        需要等待上述20个线程都计算完成后,再用main线程去的最终的结果是多少?
        只要上述20个线程还有在执行的,main线程便礼让,让他们执行,直至最后只剩main线程
         */
        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);
    }
    

    }

    class MyData {

    // volatile可以保证可见性,及时通知其它线程主物理内存的值已被修改
    volatile int number = 0;
    
    public void setTo60() {
        this.number = 60;
    }
    
    //此时number前面已经加了volatile,但是不保证原子性
    public void addPlusPlus() {
        number++;
    }
    
    // Integer 原子包装类
    AtomicInteger atomicInteger = new AtomicInteger();
    
    public void addAtomic() {
        atomicInteger.getAndIncrement();
    }
    

    } 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970

  • 程序运行结果

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

瞅瞅 AtomicInteger 源码

先获取再修改

  • getAndIncrement() 方法

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

  • getAndDecrement() 方法

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

  • getAndAdd() 方法

    public final int getAndAdd(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta); } 123

  • 总结:以上方法都通过调用 unsafe.getAndAddInt() 实现

先修改再获取

  • incrementAndGet() 方法

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

  • decrementAndGet() 方法

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

  • addAndGet() 方法

    public final int addAndGet(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta) + delta; } 123

  • 总结:以上方法都通过调用 unsafe.getAndAddInt() + delta 实现

1.2.4、代码重排

有序性

计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种

image-20200807143528418

理解指令重排序

  1. 指令重排序,就是出于优化考虑,CPU执行指令的顺序跟程序员自己编写的顺序不一致

  2. 就好比一份试卷,题号是老师规定的,是程序员规定的,但是考生(CPU)可以先做选择,也可以先做填空

  3. 单线程环境里面可以确保程序最终执行结果和代码顺序执行的结果一致

  4. 处理器在进行重排序时必须要考虑指令之间的数据依赖性

  5. 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测

重排代码示例

示例 1

  • 代码

    public void mySort(){ int x = 11; //语句1 int y = 12; //语句2 x = x + 5; //语句3 y = x * x; //语句4 } 123456

  • 以上代码,可能出现的执行顺序有1234、2134、1342,这三个都没有问题,但是语句 4 不能变成第一条,因为存在数据依赖(y 依赖于 x)。

示例 2

  1. 在代码中定义了 a, b, x, y 四个整形变量
  2. 线程 1 原本的执行顺序为 x = a; b = 1; ,线程 2 原本的执行顺序为 y = b; a = 1;
  3. 但是经过指令重排后,指令执行顺序变化,导致程序执行结果变化
  4. 这也就说明在多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的。

image-20200807145328566

示例 3

  • 代码

image-20200807150125103

分析:

  1. 变量 a 与 flag 并没有数据依赖性,所以 a = 1; 与 flag = true; 语句无法保证谁先谁后
  2. 线程操作资源类,线程1访问method1,线程2访问method2,正常情况顺序执行,a=6
  3. 多线程下假设出现了指令重排,语句2在语句1之前,当执行完flag=true后,另一个线程马上执行method2,则会输出 a=5

禁止指令重排案例小结

  1. volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象
  2. 我们先了解一个概念,内存屏障(Memory Barrfer)又称内存栅栏,是一个CPU指令,它的作用有两个:
    • 一是保证特定操作的执行顺序
    • 二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。
  3. 由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化
  4. 内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

image-20200807151131161

1.3、线程安全性保证

如何使线程安全性获得保证

  1. 工作内存与主内存同步延迟现象导致的可见性问题可以使用synchronized或volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。
  2. 对于指令重排导致的可见性问题和有序性问题可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化。

1.4、volatile 单例模式

1.4.1、DCL 单例模式

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 < 10000; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i + 1)).start();
        }
    }
    

    } 123456789101112131415161718192021222324252627282930313233343536

  • 这种写法在多线程条件下可能正确率为 99.999999%,但可能由于指令重排出错

1.4.2、单例volatile 分析

DCL 问题分析:

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

  2. 原因:可能出现某一个线程执行到第一次检测,读取到的instance不为null时,但是instance的引用对象可能没有完成初始化。原因如下:

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

    memory=allocate(); 	//1.分配对象内存空间
    instance(memory)	//2.初始化对象
    instance=memory;	//3.设置instance指向刚分配的内存地址,此时instance!=null
    123
    
  4. 步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。

    memory=allocate();	//1.分配对象内存空间
    instance=memory;	//3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成!
    instance(memory);	//2.初始化对象
    123
    
  5. 指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。

  6. 就比如说我们需要使用 instance 对象中的一个对象 heygo ,但是由于 instance 并未初始化完成,此时 heygo == null ,访问 instance.heygo 将抛出空指针异常

单例模式正确写法:

加上 volatile ,禁止指令重排

private static volatile SingletonDemo singletonDemo=null;
1

2、CAS 算法

CAS你知道吗?

2.1、CAS 概述

CAS:compare and set(比较并交换)

代码示例

  • 代码

    public class CASDemo { public static void main(String[] args) { /* CAS是什么? ==>compareAndSet 比较并交换 */ 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()); } } 12345678910111213141516171819

  • 程序运行结果

    true current data : 2019 false current data : 2019 12

分析CAS:就拿 JMM 模型来说

  1. 现在有两个线程:线程 A 和线程 B ,同时操作主内存中的变量 i
  2. 线程 A 将变量 i 的副本拷贝回自己线程的工作内存,先记录变量 i 当前的值,记录为期望值
  3. 线程 A 修改值后,将 i 的值写回主内存前,先判断一下当前主内存的值是否与期望值相等,相等我才写回,不相等证明别的线程(线程 B)改过了,如果强行写,将出现写覆盖

2.2、CAS 原理

2.2.1、Unsafe 类

CAS底层原理?如果知道,谈谈你对Unsafe的理解

一句话总结:自旋锁 + Unsafe 类

AtomicInteger 类的底层源码

  • getAndIncrement() 方法

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

  • 分析参数含义

  1. this:当前对象
  2. valueOffset:内存偏移量(内存地址)
  3. 为什么AtomicInteger能解决i++多线程下不安全的问题,靠的是底层的Unsafe类
  • AtomicInteger 类中维护了一个 Unsafe 实例,和一个 volatile 修饰的 value 值

    public class AtomicInteger extends Number implements java.io.Serializable { private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
    
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    
    private volatile int value;
    

    123456789101112131415

Unsafe 类

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

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

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

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

  5. 变量value用volatile修饰,保证了多线程之间的内存可见性

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

2.2.2、CAS 是什么

CAS 到底是个什么玩意儿?

  1. CAS的全称为Compare-And-Swap,它是一条CPU并发原语
  2. 它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
  3. CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。
  4. 再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

AtomicInteger 类 CAS 算法分析

  • 通过 AtomicInteger 类调用 getAndIncrement() 方法

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

  • atomicInteger.getAndIncrement() 方法调用 unsafe.getAndAddInt() 方法

    • this.getIntVolatile(var1,var2) 方法获取var1这个对象在var2地址上的值
    • this.compareAndSwapInt(var1, var2, var5, var5 + var4) 方法判断 var5 变量是否与期望值相同:
      • 如果 var5 与内存中的期望值相同,证明没有其他线程改过,则执行 +var 操作
      • 如果 var5 与内存中的期望值不同,证明没有其他线程改过 var2 地址处的值,然后再重新获取 var2 地址处的值,重复 compare and set 操作

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

    return var5;
    

    } 12345678

  • 总结:getAndIncrement()方法底层调用的是Unsafe类的getAndAddInt()方法,底层是CAS思想

atomicInteger.getAndIncrement() 方法详解

  • AtomicInteger 类的 getAndIncrement() 方法

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

  • Unsafe 类的 getAndAddInt() 方法

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

    return var5;
    

    } 12345678

流程分析:

  1. var1:Atomiclnteger对象本身。
  2. var2:该对象值得引用地址。
  3. var4:需要变动的数量。
  4. var5:使用var1 var2找出的主内存中真实的值。
  5. 用该对象当前的值与var5比较:
    • 如果相同,更新var5 + var4并且返回true,
    • 如果不同,继续取值然后再比较,直到更新完成。

举例说明:

  1. 假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上):
  2. AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本,分别拷贝到各自的工作内存
  3. 线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。
  4. 线程B也通过getIntVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。
  5. 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值己经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
  6. 线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwaplnt进行比较替换,直到成功。

底层汇编指令

  1. Unsafe类中的compareAndSwapInt,是一个本地方法,该方法的实现位于unsafe.cpp中
  2. Atomic:cmpxchg 指令:但凡带 Atomic 汇编指令都是不会被其他线程打断

image-20200807170454109

CAS 简单小总结

CAS(CompareAndSwap)

比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止

CAS应用

  1. CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。
  2. 当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

2.3、CAS 缺点

1、循环时间长开销很大

我们可以看到getAndAddInt方法执行时,有个do while

如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

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

    return var5;
}
12345678

2、只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。

3、引出来ABA问题?

2.4、面试题

为什么用 CAS 而不用synchronized?

以下是我的理解

  1. 使用 synchronized 虽然能保证操作的原子性,但是将操作变成了串行操作,大大降低了程序的并发性
  2. 如果使用 synchronized 没有抢到同步锁,那么线程将处于阻塞状态,等待 CPU 的下一次调度
  3. CAS 使用 Unsafe 类 + 自旋锁实现操作的原子性,Unsafe 类中使用 do while 循环实现 compare and set ,多个线程可以同时操作,大大提高了程序的并发性,并且不存在让线程等待的问题

3、ABA 问题

原子类AtomicInteger的ABA问题?原子更新引用知道吗?

3.1、ABA 问题的产生

面试坑爹套路

CAS —> UnSafe —> CAS底层思想 —> ABA —> 原子引用更新 —> 如何规避ABA问题

ABA问题是怎样产生的?

CAS会导致 ABA 问题

  1. CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
  2. 比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。
  3. 尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
  4. 一句话总结:狸猫换太子

3.2、原子引用

原子引用代码示例

  • 代码:使用 AtomicReference 原子引用类封装我们自定义的 User 类

    /**

    • @ClassName AtomicReferenceDemo

    • @Description TODO

    • @Author Heygo

    • @Date 2020/8/7 18:45

    • @Version 1.0 */ public class AtomicReferenceDemo {

      public static void main(String[] args) { AtomicReference atomicReference = new AtomicReference<>();

       User z3 = new User("z3", 23);
       User l4 = new User("l4", 24);
       User w5 = new User("w5", 25);
      
      
       atomicReference.set(z3);
       System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
       System.out.println(atomicReference.compareAndSet(z3, w5) + "\t" + atomicReference.get().toString());
      

      }

    }

    class User {

    String userName;
    int age;
    
    public User(String userName, int age) {
        this.userName = userName;
        this.age = age;
    }
    
    @Override
    public String toString() {
        return "User{" +
                "userName='" + userName + '\'' +
                ", age=" + age +
                '}';
    }
    

    } 123456789101112131415161718192021222324252627282930313233343536373839404142

  • 程序运行结果

    true User{userName='l4', age=24} false User{userName='l4', age=24} 12

3.3、版本号原子引用

解决ABA问题:理解原子引用 + 新增一种机制,那就是修改版本号(类似时间戳)

  • 代码:使用带版本号的原子类 AtomicStampedReference 解决 ABA 问题

    /**

    • @ClassName ABADemo

    • @Description TODO

    • @Author Heygo

    • @Date 2020/8/7 21:08

    • @Version 1.0 */ public class ABADemo { // 初始值为 100 static AtomicReference atomicReference = new AtomicReference<>(100); // 初始值为 100 ,初始版本号为 1 static AtomicStampedReference 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(() -> {
           // 暂停1秒钟线程2,保证上面t1线程完成一次ABA操作
           try {
               TimeUnit.SECONDS.sleep(1);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
       }, "t2").start();
      
       // 保证上面的操作执行完成
       while (Thread.activeCount() > 2) {
           Thread.yield();
       }
      
       System.out.println("======以下是ABA问题的解决=====");
       new Thread(() -> {
           System.out.println(Thread.currentThread().getName() + "\t第1次版本号:" + atomicStampedReference.getStamp());
           // 暂停1秒钟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);
           // 暂停3秒钟t4线程,保证上面t3线程完成一次ABA操作
           try {
               TimeUnit.SECONDS.sleep(3);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, atomicStampedReference.getStamp() + 1);
           System.out.println(Thread.currentThread().getName() + "\t修改成功否: " + result + "\t当前最新实际版本号:" + atomicStampedReference.getStamp());
           System.out.println(Thread.currentThread().getName() + "\t当前实际值:" + atomicStampedReference.getReference());
       }, "t4").start();
      

      } }

    1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768

  • 程序运行结果

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

关于 AtomicStampedReference 的一些说明

  • AtomicStampedReference 的构造器

    • initialRef:初始值
    • initialStamp:初始版本号

    public AtomicStampedReference(V initialRef, int initialStamp) { pair = Pair.of(initialRef, initialStamp); } 123

  • compareAndSet() 方法

    • expectedReference:期望值

    • newReference:新值

    • expectedStamp:期望版本号

    • newStamp:新的版本号

      public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) { Pair current = pair; return expectedReference == current.reference && expectedStamp == current.stamp && ((newReference == current.reference && newStamp == current.stamp) || casPair(current, Pair.of(newReference, newStamp))); } 123456789101112