前言需求
我们来看看不同大厂直接涉及到的一些有关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.加锁与解锁要同一把锁
什么玩意?又多两个知识点,什么是主内存、工作内存?
对于我们工作中的数据存储大概是这样:硬盘<内存
比如说当我们的小明同学存储在主内存中
这时有三个线程需要修改小明的年龄,那么会怎么操作呢?
假如线程t1,将小明的年龄修改为:37,这时会怎么样呢?
我们需要一种机制,能知道某线程操作完后写回主内存及时通知其他线程 简单的来说:
结论: 由于 而 但线程对 操作完成后再 因此
结论: 但是有没有发现,当将number 修改为60的时候,main主线程并不知道 所以我们的main线程需要被通知,需要被第一时间看见修改的情况 这时我们的main 接收到最新情况,并没有进入while循环,没有像刚刚那样一直傻傻的等待,所以直接getMessage输出最新的值 首先我们来看看什么是原子性? 指的是: 比如说:来上课同学在黑板上签到自己的名字,是不能被打断或者修改的
那么我们上面根据官方的定义总结一句话说明,提到过不保证原子性 我们前面说到使用volatile 可以让其他线程第一时间看到最新情况 但是这也是不好的地方,我们用案例来说说这种情况是怎么回事 我们采用for循环来模拟二十个线程,每个线程做1000次的调用方式 我们使用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执行第三步将值写回给主内存时
1.添加synchronized的方式 为什么使用AtomicInteger可以解决这个问题呢?(小编水平不够,后面再补) 那么我们来聊聊什么是指令重排,什么是指令重排? 其实就是有序性,简单的来说程序员一般写的代码长这样
但是在我们的电脑机器眼里,我们的代码长这样
换句话说,什么是指令重排的呢?
为了保证快、准、稳,会做一些指令重排提高性能
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。 处理器在进行重排序时必须要考虑指令之间的 多线程环境中载程交替执行,由于编泽器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的结果无法预测 示例一:班上同学答题
我们有五道题 当只有一个同学的时候,我们可以随便抢,都是一题一题有执行顺序 当有多个同学的时候,我们无法控制顺序,抢到哪一题就是哪一题 示例二:代码块执行顺序 当我们单线程的时候,他的顺序是1234 当我们多线程的时候,他有可能顺序就是:2134、1234了 那么请问:执行顺序可以是4132、4123呢? 答案是不可以的,因为必须要考虑指令之间的 示例三:代码执行顺序
请问x y 是多少?答: 如果编译器对这段代码进行重新优化后,可能会出现以下情况
请问x y 是多少?答: 示例四:代码块执行顺序 假如示例代码出现指令重排的情况,语句1,语句2 的顺序便从1-2,变成2-1,这个时候flag = true 当两个线程有一个线程抢到flag = true 就会执行下面的if判断 这时就会有两个结果:a = 6 、a =5 volatile禁止实现指令重排优化,从而避免多线程下程序出现乱序执行的现象 先了解一个概念, 1. 2.保证 由于 也就是说 内存屏障另外一个作用是
我们都知道单例模式下懒汉有非线程安全的情况发生,常见的方式下采用DCL(Double Check Lock) 那么多线程情况下会指令重排导致读取的对象是半初始化状态情况 其实DCL机制也不一定是线程安全的情况,原因是 原因在于某一个线程执行第一次检测的时候有以下情况 简单来说:分座位,有一个叫张三的人一个小时候才来坐这个位置 理论上座位分配出去了,但实际上人并没有到,有名无实 我们来分析一下instance = new Singleton(); 这一步代码 简单来说对应的步骤是 虽然说这是理论上来说是这样的,但是很抱歉 指令重后变成了以下的执行顺序 但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。 所以当一条线程访问instance不为nul时,由于instance实例未必已初始化完成,也就造成了线程安全问题。 即示例未初始化完成,保留的是默认值,这样也是出问题 所以使用volatile禁止指令重排,老老实实按顺序来 尚硅谷:Java大厂面试题全集(周阳主讲):volatile比如下一节我们班的语文课修改为数学课,需要马上通知给我们班所有同学,下节课改为数学课了
只要有变动,立即收到最新消息
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 in
AAA updated number value: 60
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 in
AAA updated number value: 60
main getMessage number value: 60
三、volatile的原子性特征
不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。 需要整体完整要么同时成功,要么同时失败
。那么为什么会出现不保证原子性呢?
class TestData{
volatile int number = 0;
//当方法调用的时候,number值++
public void addNumPlus(){
number ++;
}
}
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怎么解决原子性问题
2.使用AtomicIntegerclass 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:19966
main AtomicInteger finally number value:20000
四、volatile的指令重排
数据依赖性
public void mySort()
{
int x=11; //语句1
int y=12; //语句2
x= x + 5; //语句3
y= x * x; //语句4
}
数据依赖性
x = 0 y = 0
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);
}
}
volatile 禁止实现指令重排优化
内存屏障(Memory Barrier)又称内存栅栏
,是一个CPU指令,他的作用有两个作用:保证特定操作的执行顺序
某些变量的内存可见性(利用该特性实现volatile的内存可见性)
编译器和处理器都能执行指令重排优化
。如果在指令间插入一条MemoryBarrier则会告诉编译器和CPU,不管什么指令都不能让这条Memory Barrier指令重排序
。通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化
。强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本
。五、单例模式下的volatile
public static Singleton getInstance() {
if(instance == null) {
synchronized (Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
有指令重排
1.读取到instance !=null
2.instance 的引用对象没有完成初始化
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.初始化对象
参考资料