前言需要
咱们来看看不同大厂间接波及到的一些无关volatile的面试题
蚂蚁花呗:请你谈谈volatile的工作原理
今日头条:Volatile的禁止指令重排序有什么意义?synchronied怎么用?
蚂蚁金服:volatile 的原子性问题?为什么i++ 这种不反对原子性?从计算机原理的设计来讲下不能保障原子性的起因
一、volatile 是什么?
Java语言标准第三版中对volatile的定义如下:java编程语言容许线程访问共享变量,为了确保共享变量能被精确和统一的更新,线程应确保通过排他锁独自取得这个变量
。
Java语言提供了volatile,在某些状况下比锁更加不便。
如果一个字段被申明为volatile,java线程内存模型确保所有线程看到这个变量的值是统一的
从下面的官网定义咱们可简略一句话阐明:volatile是轻量级的同步机制次要有三大个性:保障可见性、不保障原子性、禁止指令重排
那么仅接问题就来了:什么是可见性?什么是不保障原子性?指令重排请你说说?
二、volatile 的可见性特色
在说可见性之前,咱们须要从JMM开始说起,不然怎么讲?
咱们晓得JVM是Java虚拟机,JMM是什么?答:Java内存模型
那么JMM与volatile有什么关系?
别急,咱们先来理解一下JMM是个什么玩意先
JMM(Java内存模型Java Memory Model,简称JMM)自身是一种形象的概念并不实在存在,它形容的是一组规定或标准
。
通过这组标准定义了程序中各个变量(包含实例字段,动态字段和形成数组对象的元素)的拜访形式
。
简略来说就像中国的十二生肖,其中有一龙,但你能在动物园里牵一头进去吗?
这龙其实就是十二生肖之一,是一种标准,占位,约定,有一个地位属龙
JMM对于对于同步的规定
1.线程解锁前
,必须把共享变量的值刷新回主内存
2.线程加锁前
,必须读取主内存的最新值
到本人的工作内存
3.加锁与解锁要同一把锁
什么玩意?又多两个知识点,什么是主内存、工作内存?
对于咱们工作中的数据存储大略是这样:硬盘<内存<CPU
比如说当咱们的小明同学存储在主内存中
这时有三个线程须要批改小明的年龄,那么会怎么操作呢?
如果线程t1,将小明的年龄批改为:37,这时会怎么样呢?
咱们须要一种机制,能晓得某线程操作完后写回主内存及时告诉其余线程
简略的来说:比方下一节咱们班的语文课批改为数学课,须要马上告诉给咱们班所有同学,下节课改为数学课了
论断:只有有变动,立刻收到最新消息
JMM的主内存与工作内存形容
因为JVM运行程序的实体是线程
,而每个线程创立时JVM都会为其创立一个工作内存(有些中央称为栈空间)
,工作内存是每个线程的公有数据区域
。
而Java内存模型
中规定所有变量都存储在主内存
,主内存是共享内存区域
,所有线程都能够拜访
但线程对变量的操作(读取赋值等)必须在工作内存中进行
,首先要将变量从主内存拷贝的本人的工作内存空间
,而后对变量进行操作
操作实现后再将变量写回主内存
,不能间接操作主内存中的变量
,各个线程中的工作内存中存储着主内存中的变量正本拷贝
。
因而不同的线程间
无奈拜访对方的工作内存
,线程间的通信(传值)必须通过主内存来实现
,其简要拜访过程如下图:
论断:当某线程批改完后并写回主内存后,其余线程第一工夫就能看见,这种状况称:可见性
示例代码来意识可见性
class TestData{ int number = 0; //当办法调用的时候,number值改为60 public void addNum(){ this.number = 60; }}
public static void main(String[] args) { TestData data = new TestData(); new Thread(() -> { System.out.println(Thread.currentThread(). getName()+"\t come in"); try { //暂停一会 TimeUnit.SECONDS.sleep(3 ); } catch (InterruptedException e) { e.printStackTrace(); } //调用办法从新赋值 data.addNum(); System.out.println(Thread.currentThread(). getName()+"\t updated number value: " +data.number); },"AAA"). start(); while(data.number == 0){ } System.out.println(Thread.currentThread(). getName()+"\t getMessage number value: "+data.number); } 运行后果如下:AAA come inAAA updated number value: 60
然而有没有发现,当将number 批改为60的时候,main主线程并不知道
所以咱们的main线程须要被告诉,须要被第一工夫看见批改的状况
class TestData{ volatile int number = 0; //当办法调用的时候,number值改为60 public void addNum(){ this.number = 60; }}
public static void main(String[] args) { TestData data = new TestData(); new Thread(() -> { System.out.println(Thread.currentThread(). getName()+"\t come in"); try { //暂停一会 TimeUnit.SECONDS.sleep(3 ); } catch (InterruptedException e) { e.printStackTrace(); } //调用办法从新赋值 data.addNum(); System.out.println(Thread.currentThread(). getName()+"\t updated number value: " +data.number); },"AAA"). start(); while(data.number == 0){ } System.out.println(Thread.currentThread(). getName()+"\t getMessage number value: "+data.number); } 运行后果如下:AAA come inAAA updated number value: 60main getMessage number value: 60
这时咱们的main 接管到最新状况,并没有进入while循环,没有像刚刚那样始终傻傻的期待,所以间接getMessage输入最新的值
三、volatile的原子性特色
首先咱们来看看什么是原子性?
指的是:不可分割,完整性,也即某个线程正在做某个具体业务时,两头不能够被加塞或者被宰割。 须要整体残缺要么同时胜利,要么同时失败
。
比如说:来上课同学在黑板上签到本人的名字,是不能被打断或者批改的
那么咱们下面依据官网的定义总结一句话阐明,提到过不保障原子性
那么为什么会呈现不保障原子性呢?
咱们后面说到应用volatile 能够让其余线程第一工夫看到最新状况
然而这也是不好的中央,咱们用案例来说说这种状况是怎么回事
class TestData{ volatile int number = 0; //当办法调用的时候,number值++ public void addNumPlus(){ number ++; }}
咱们采纳for循环来模仿二十个线程,每个线程做1000次的调用形式
public static void main(String[] args) { TestData data = new TestData(); for (int i = 1; i<= 20; i++){ new Thread(() -> { for (int j= 1; j<= 1000; j++){ //调用办法从新赋值 data.addNumPlus(); } },String.valueOf(i)). start(); } //须要期待下面20个线程都全副计算实现后,再main线程获得最终的后果值看是多少? while(Thread . activeCount() > 2){ Thread.yield(); } System.out.println(Thread.currentThread().getName()+"\t finally number value"+ data.number);运行后果如下:main finally number value19853
咱们应用volatile 来保障可见性,按理来说20个线程每个做1000次
咱们的到的后果应该是20000才对,为什么是19853呢??why!
图解为什么不保障原子性
还记得咱们的JMM规定所有变量都存储在主内存
,而线程对变量的操作(读取赋值等)必须在工作内存中进行
吗?
首先要将变量从主内存拷贝的本人的工作内存空间
,而后对变量进行操作
所以咱们以后的t1、t2、t3 初始值为0
当咱们的线程调用办法进行++的时候,拷贝正本到本人的内存空间
比如说t1、t2、t3线程各自在本人的空间++完后,将变量写回主内存
这时因为线程之间交织,在某一时间段内呈现了一些问题
导致被t2 线程写入主内存,刷新数据写回主内存
咱们volatile保障了可见性,这时应该是第一工夫告诉其余线程
这也就是为什么不与咱们想的一样,是20000,反而是19853
图解解读字节码++操作做了哪些事件
这里应用新的类T1,抽取进去但等同代码是一样的
咱们依据add 办法的事件,先看看它做了哪些事件
噢,看了剖析图,是否理解了理论n++ 分了三步骤
当咱们的线程t1、t2、t3执行第一步拷贝正本到本人空间
当咱们的线程t1、t2、t3执行第二步在本人空间操作变量
当咱们的线程t1、t2、t3执行第三步将值写回给主内存时
volatile怎么解决原子性问题
1.增加synchronized的形式
2.应用AtomicInteger
class TestData{ volatile int number = 0; //当办法调用的时候,number值改为60 public void addNum(){ this.number = 60; } public void addNumPlus(){ number ++; } AtomicInteger atomicInteger = new AtomicInteger(); public void addAtomic(){ atomicInteger.getAndIncrement(); }}
public static void main(String[] args) { TestData data = new TestData(); for (int i = 1; i<= 20; i++){ new Thread(() -> { for (int j= 1; j<= 1000; j++){ //调用办法从新赋值 data.addNumPlus(); data.addAtomic(); } },String.valueOf(i)). start(); } //须要期待下面20个线程都全副计算实现后,再main线程获得最终的后果值看是多少? while(Thread . activeCount() > 2){ Thread.yield(); } System.out.println(Thread.currentThread().getName()+"\t int finally number value:"+ data.number); System.out.println(Thread.currentThread().getName()+"\t AtomicInteger finally number value:"+ data.atomicInteger);}运行后果如下:main int finally number value:19966main AtomicInteger finally number value:20000
为什么应用AtomicInteger能够解决这个问题呢?(小编程度不够,前面再补)
四、volatile的指令重排
那么咱们来聊聊什么是指令重排,什么是指令重排?
其实就是有序性,简略的来说程序员个别写的代码长这样
然而在咱们的电脑机器眼里,咱们的代码长这样
换句话说,什么是指令重排的呢?
为了保障快、准、稳,会做一些指令重排进步性能
单线程环境外面确保程序最终执行后果和代码程序执行的后果统一。
处理器在进行重排序时必须要思考指令之间的数据依赖性
多线程环境中载程交替执行,因为编泽器优化重排的存在,两个线程中应用的变量是否保障一致性是无奈确定的后果无奈预测
示例一:班上同学答题
咱们有五道题
当只有一个同学的时候,咱们能够轻易抢,都是一题一题有执行程序
当有多个同学的时候,咱们无法控制程序,抢到哪一题就是哪一题
示例二:代码块执行程序
public void mySort(){ int x=11; //语句1 int y=12; //语句2 x= x + 5; //语句3 y= x * x; //语句4}
当咱们单线程的时候,他的程序是1234
当咱们多线程的时候,他有可能程序就是:2134、1234了
那么请问:执行程序能够是4132、4123呢?
答案是不能够的,因为必须要思考指令之间的数据依赖性
示例三:代码执行程序
请问x y 是多少?答:x = 0 y = 0
如果编译器对这段代码进行从新优化后,可能会呈现以下状况
请问x y 是多少?答:x = 2 y = 1
示例四:代码块执行程序
public class ReSortSeqDemo{ int a=0; boolean flag = false; public void method01(){ a =1; //语句1 flag = true;//语句2 } public void method02(){ if(flag){ a=a+5; //语句3 } System.out.println("*****retValue: "+a); }}
如果示例代码呈现指令重排的状况,语句1,语句2 的程序便从1-2,变成2-1,这个时候flag = true
当两个线程有一个线程抢到flag = true 就会执行上面的if判断
这时就会有两个后果:a = 6 、a =5
volatile 禁止实现指令重排优化
volatile禁止实现指令重排优化,从而防止多线程下程序呈现乱序执行的景象
先理解一个概念,内存屏障(Memory Barrier)又称内存栅栏
,是一个CPU指令,他的作用有两个作用:
1.保障特定操作的执行程序
2.保障某些变量的内存可见性(利用该个性实现volatile的内存可见性)
因为编译器和处理器都能执行指令重排优化
。如果在指令间插入一条MemoryBarrier则会通知编译器和CPU,不论什么指令都不能让这条Memory Barrier指令重排序
。
也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化
。
内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因而任何CPU上的线程都能读取到这些数据的最新版本
。
五、单例模式下的volatile
咱们都晓得单例模式下懒汉有非线程平安的状况产生,常见的形式下采纳DCL(Double Check Lock)
public static Singleton getInstance() { if(instance == null) { synchronized (Singleton.class) { if(instance == null) { instance = new Singleton(); } } } return instance;}
那么多线程状况下会指令重排导致读取的对象是半初始化状态状况
其实DCL机制也不肯定是线程平安的状况,起因是有指令重排
起因在于某一个线程执行第一次检测的时候有以下状况
1.读取到instance !=null
2.instance 的援用对象没有实现初始化
简略来说:分座位,有一个叫张三的人一个小时候才来坐这个地位
实践上座位调配进来了,但实际上人并没有到,有名无实
咱们来剖析一下instance = new Singleton(); 这一步代码
1.memory = allocate(); //1. 调配对象内存空间
2.instance(memory);//2.初始化对象
3.instance = memory; //3. 设置instance指向刚调配的内存地址,此时instance! =null
简略来说对应的步骤是
1.有个张三的须要调配座位,我为给他留一个地位
2.给张三的地位调配好网线,电脑,擦洁净桌子
3.一个小时后张三到了把地位给他坐下,进行上课
尽管说这是实践上来说是这样的,然而很道歉步骤2和步骤3不存在数据依赖关系
,而且无论重排前还是重排后程序的执行后果在单线程中并没有扭转
,因而这种重排优化是容许的。
指令重后变成了以下的执行程序
1.memory = allocate(); //1. 调配对象内存空间
2.instance = memory; //3. 设置instance指向刚调配的内存地址,此时instance! =null
3.instance(memory);//2.初始化对象
然而指令重排只会保障串行语义的执行的一致性(单线程),但并不会关怀多线程间的语义一致性。
所以当一条线程拜访instance不为nul时,因为instance实例未必已初始化实现,也就造成了线程平安问题。
即示例未初始化实现,保留的是默认值,这样也是出问题
所以应用volatile禁止指令重排,老老实实按程序来
参考资料
尚硅谷:Java大厂面试题选集(周阳主讲):volatile