面试之Volatile-CAS-ABA
一、Volate:
1. 请谈谈你对Volate的理解:
1. volate是Java虚拟机提供的轻量级同步机制
三大特性:保证可见性、不保证原子性、禁止指令重排
2. JMM你谈谈:
JMM(Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在, 他描述的是一组规则或规范,通过这个规范定义了程序中的各个变量(包括实例字段,对象字段和构成数组对象的元素)的访问方式;通过可见性、原子性、有序性来保证线程安全。
JMM关于同步的规定:
1. 线程解锁前,必须把共享变量的值刷回主内存;
2. 线程加锁前,必须读取主内存的最新值到自己的工作内存;
3. 加锁解锁是同一把锁;
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间),工作内存是内个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后将变量写回主内存,不能直接操作主内存中的变量,各个线程的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图: 硬盘 < 内存 < 缓存 < CPU
《===》
2.1 可见性:所谓的可见性就是一种及时通知机制
通过前面的JMM介绍,我们知道各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的。
这就可能存在一个线程AA修改了共享变量X的值,但是还未写回到主内存时,另一个线程BB又对主内存中的同一个变量X进行操作,但是此时A线程工作内存中的共享变量X对线程B来说并不可见,这种工作内存与主内存同步延迟现象造成了可见性问题。
代码验证:
package com.example.code.valatile; import java.util.concurrent.TimeUnit; class MyDate { volatile int number = 0 ; public void addTo60() { this.number = 60;} } /** * 1. 验证volatile的可见性 * 1.1 假如int number =0;number之前根本没有添加volatile关键字修饰,没有可见性 */ public class VolatileDemo { public static void main(String[] args) { MyDate myDate = new MyDate(); //资源类,线程操作资源类 new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t come in"); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } myDate.addTo60(); System.out.println(Thread.currentThread().getName() + "\t Update number value " + myDate.number); },"AAA").start(); while (myDate.number == 0) {} //main线程一直循环,直至number不为空 System.out.println(Thread.currentThread().getName() + "\t mission is over" + myDate.number); } }
2.2 原子性:volatile不保证原子性
number++在多线程下是非线程安全的,如果不加synchronized解决?
* 加synchronized;
* 使用JUC下面的AtomicInteger
代码验证:
package com.example.code.valatile; import java.util.concurrent.TimeUnit; class MyDate { volatile int number = 0 ; public void addTo60() { this.number = 60;} public void addPlusPlus() { number++;} } /** * 1. 验证volatile的可见性 * 1.1 假如int number =0;number之前根本没有添加volatile关键字修饰,没有可见性 * 1.2 添加了Volatile,可以解决可见性问题; * * 2. 验证volatile的原子性: * 2.1 原子性指的是什么意思? * 不可分割,完整性,及某个线程正在做某个具体业务时,中间不可以被加塞或者分割,需要整体完整,要么同时成功,要么同时失败 * 2.2 volatile不保证原子性的案例演示: * * 2.3 why * * 2.4 如何解决原子性? * * 加sync * * 使用我们的juc下AtomicInteger */ public class VolatileDemo { public static void main(String[] args) { MyDate myDate = new MyDate(); for (int i = 0; i < 20; i++) { new Thread(()->{ for (int j = 0; j < 1000; j++) { myDate.addPlusPlus(); } },String.valueOf(i)).start(); } while (Thread.activeCount() > 2) { Thread.yield(); } System.out.println(Thread.currentThread().getName() + "\t finally number value:" + myDate.number); } //volatile可以保证可见性,及时通知其他线程,主物理内存的值已经被修改 private static void seeOkByVolatile() { MyDate myDate = new MyDate(); //资源类,线程操作资源类 new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t come in"); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } myDate.addTo60(); System.out.println(Thread.currentThread().getName() + "\t Update number value " + myDate.number); },"AAA").start(); while (myDate.number == 0) {} //main线程一直循环,直至number不为空 System.out.println(Thread.currentThread().getName() + "\t mission is over" + myDate.number); } }
2.3 有序性:
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排,一般分一下三种:
源代码 ==》 编译器优化重排 ==》 指令并行重排 ==》 内存系统重排 ==》 最终执行指令
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致;
处理器在进行重排序时必须考虑到指令之间的数据依懒性;
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测;
代码验证:重排1
public void mySort() { int x = 11; //语句1 int y = 12; //语句2 x = x + 5; //语句3 y = x * x; //语句4 } 1234 2134 1324 问题:请问语句4可以重排后变成第一条吗? 不可以,数据依懒性;
代码验证:重排2
package com.example.code.valatile; public class ReSortSeqDemo { int a = 0 ; boolean flag = false ; public void method01() { a = 1 ; //语句1 flag = true ; //语句2 } //多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测 public void method02() { if (false) { a = a + 5 ; //语句3 System.out.println("***returnVal:" + a); } } }
禁止指令重排小总结:
volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。
先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:
一是保证特定操作的执行顺序;
二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)
由于编译器和处理器都能执行指令重排优化。如果在指令间插入Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障的另外一个作用是强制刷出CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本;
3. 你在那些地方用到过volatile:(单例,读写锁手写缓存,CAS)
3.1 单例模式DCL代码:(Double check Lock 双端检索机制)
package com.example.code.valatile; public class SingletonDemo { private static volatile SingletonDemo instance = null; private SingletonDemo(){ System.out.println("我是构造方法"); } private static SingletonDemo getInstance() { if(instance == null) { synchronized (SingletonDemo.class) { if (instance == null) { instance = new SingletonDemo(); } } } return instance; } public static void main(String[] args) { //单线程 /* System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());*/ //多线程 for (int i = 0; i < 100; i++) { new Thread(() -> {SingletonDemo.getInstance();},String.valueOf(i)).start(); } } }
3.2 单例模式volatile分析:
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实例未必已初始化完成,也就造成了线程安全性问题。
*****************************************************************************************************************8
二、 CAS你知道吗? 期望值、真实值、更新值
1. 比较并交换:
AtomicInteger atomicInteger = new AtomicInteger(5); //期望值=真实值,返回true,set为更新值 System.out.println(atomicInteger.compareAndSet(5, 2019) + "\t current data:" + atomicInteger.get()); //期望值 != 真实值,返回false,放弃之前操作,从主物理内存中拿最新值,重新进行比较 System.out.println(atomicInteger.compareAndSet(5, 1024) + "\t current data:" + atomicInteger.get()); atomicInteger.getAndIncrement();
----------------------------------------------------------------------------------------------------
2. CAS底层原理?如果知道,谈谈你对UnSafe的理解(自旋锁、UnSafe类)
2.1 atomicInteger.getAndIncrement();
2.2 Unsafe:
1. Unsafe:
来自于JVM rt.jar允许jar包里,是CAS的核心类,由于Java无法直接访问底层操作系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
注意:Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务;
2. 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
3. 变量value用volatile修饰,保证了多线程之间的内存可见性。
2.3 CAS是什么:
CAS的全称为Compare-And-Swap,它是一条CPU并发原发。
它的功能是判断内存某个位置的值是否为预期值,如果是则改为新的值,这个过程是原子的。
CAS并发原语体现在Java语言中就是sun.misc.Unsafe类中的各个方法。调用Unsafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。
这是一种完全一览与于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干
条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原
子指令,不会造成所谓的数据不一致问题。
1. unsafe.getAndAddInt:
var1 AtomicInteger对象本身。
var2 该对象值得引用地址。
var4 需要变动的数量。
var5 用var1和var2找出主内存中真实的值。
用该对象当前的值与var5比较,如果相同,更新var5+var4并返回true;如果不同,继续取值然后比较,知道更新完成;
假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同的CPU上):
1. AtomicInteger里面的value原始值为3,即主内存中AtmoicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份价值为3的value的副本分别到各自的工作内存。
2. 线程A通过getIntVolatile(var1, var2)拿到value3,这时线程A被挂起。
3. 线程B也通过getIntVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法,比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。
4. 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值已经被其他线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
5. 线程A重新获取value值,应为变量value被volatile修饰,所以其他线程对它的修改,线程A总是能够看到的,线程A继续执行compareAndSwapInt进行比较替换,直到成功。
2. 底层汇编:
3. 简单版小总结:
CAS(CompareAndSwap):
比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止。
CAS应用:
CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。而且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做 只能保证一个共享变量的原子操作;
--------------------------------------------------------------------------------------------------------------------
3. CAS缺点:
3.1 循环时间长,开销很大;
3.2 只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作。但是,对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用加锁来保证原子性。
3.3 引出ABA问题???
************************************************************************************************************
三、原子类AtmoicInteger的ABA问题谈谈?原子更新引用知道吗?
AtomicInteger: CAS ---> Unsafe ---> CAS底层思想 ---> ABA ---> 原子引用更新 ---> 如果规避ABA问题
* ABA:狸猫换太子 出轨/银行交易 根据业务需求不同来选择,如果不介意中间发生过操作,可以忽略,如果介意可以引入原子引用,及相应的原子引用时间戳问题;
1. CAS问题怎么产生的:
CAS会导致“ABA问题”。CAS算法实现一个重要前提,需要取出内存中某时刻数据并在当下时刻比较并替换,那么这个时间差会导致数据的变化。
比如说一个线程one从内存位置V中取出A,这个时候另一个线程tow也从内存中取出A,并且线程tow进行了一些操作将值变成了B,然后线程tow又将V位置的数据变成A,
这个时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。
尽管线程one的CAS操作成功,但是不代表这个过程就没有问题;
2. 原子引用:
@Getter @ToString @AllArgsConstructor class User { String userName ; int age ; } public class AtomicReferenceDemo { public static void main(String[] args) { User zs = new User("zs", 18); User ls = new User("ls", 10); AtomicReference<User> atomicReference = new AtomicReference<>(); atomicReference.set(zs); System.out.println(atomicReference.compareAndSet(zs, ls) + "\t " + atomicReference.get().toString()); System.out.println(atomicReference.compareAndSet(zs, ls) + "\t " + atomicReference.get().toString()); } }
3. 时间戳原子引用:
package com.example.code.atomic; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicStampedReference; public class ABADemo { //ABA问题的解决 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); }, "A").start(); new Thread(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get()); }, "B").start(); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("=====以下是ABA问题的解决======"); new Thread(() -> { int result = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName() + "\t原始版本号:" + result); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } atomicStampedReference.compareAndSet(100, 101, result, result+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()); }, "C").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 compareAndSet = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1); System.out.println(Thread.currentThread().getName() + "\t修改成功否:" + compareAndSet + "\t当前实际版本号:" + atomicStampedReference.getStamp()); System.out.println(Thread.currentThread().getName() + "\t当前最新值:" + atomicStampedReference.getReference()); }, "D").start(); } }
解决ABA问题:理解原子引用 + 新增一种机制,那就是修改版本号(类似于时间戳)