Java中volatile用来做什么?
Java中volatile用来干啥?Volatile是Java虚拟机提供的轻量级的同步机制(三大特性):保证可见性不保证原子性禁止指令重排要理解三大特性,就必须知道Java内存模型(JMM),那JMM又是什么呢?JMM又是啥?...
一、Java中volatile用来做什么?
Volatile是Java虚拟机提供的轻量级的同步机制(三大特性):
- 保证可见性
- 不保证原子性
- 禁止指令重排
要理解三大特性,就必须知道Java内存模型(JMM),那JMM又是什么呢?
二、JMM又是什么?
为什么需要Java内存模型?
屏蔽各种硬件和操作系统的内存访问差异
JMM是Java内存模型,也就是Java Memory Model,简称JMM,本身是一种抽象的概念,实际上并不存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
到底什么是Java内存模型?
-
定义程序中各种变量的访问规则
-
把变量值存储到内存的底层细节
-
从内存中取出变量值的底层细节
Java内存模型的两大内存是啥?
-
主内存
-
Java堆中对象实例数据部分
-
对应于物理硬件的内存
-
-
工作内存
-
Java栈中的部分区域
-
优先存储于寄存器和高速缓存
-
Java内存模型是怎么做的?
Java内存模型的几个规范:
-
1.所有变量存储在主内存
-
2.主内存是虚拟机内存的一部分
-
3.每条线程有自己的工作内存
-
4.线程的工作内存保存变量的主内存副本
-
5.线程对变量的操作必须在工作内存中进行
-
6.不同线程之间无法直接访问对方工作内存中的变量
-
7.线程间变量值的传递均需要通过主内存来完成
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写会主内存
,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程:
Java内存模型的三大特性
-
可见性(当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改)
-
原子性(一个操作或一系列操作是不可分割的,要么同时成功,要么同时失败)
-
有序性(变量赋值操作的顺序与程序代码中的执行顺序一致)
关于有序性:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内似表现为串行的语义”(Within-Thread As-If-Serial Semantics),后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。
三、那为什么其他线程能感知到变量更新?
其实这里就是用到了“窥探(snooping)”协议。在说“窥探(snooping)”协议之前,首先谈谈缓存一致性的问题。
缓存一致性
当多个CPU持有的缓存都来自同一个主内存的拷贝,当有其他CPU偷偷改了这个主内存数据后,其他CPU并不知道,那拷贝的内存将会和主内存不一致,这就是缓存不一致。那我们如何来保证缓存一致呢?这里就需要操作系统来共同制定一个同步规则来保证,而这个规则就有MESI协议。
MESI
当CPU写数据时,如果发现操作的变量是共享变量,即在其它CPU中也存在该变量的副本,系统会发出信号通知其它CPU将该内存变量的缓存行
设置为无效。
当其它CPU读取这个变量的时,发现自己缓存该变量的缓存行是无效的,那么它就会从内存中重新读取。
总线嗅探
那其他CPU是怎么知道要将缓存更新为失效的呢?这里是用到了总线嗅探技术。
每个CPU不断嗅探总线上传播的数据来检查自己缓存值是否过期了,如果处理器发现自己的缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行修改操作的时候,会重新从内存中把数据读取到处理器缓存中。
总线风暴
总线嗅探技术有哪些缺点?
由于MESI缓存一致性协议,需要不断对主线进行内存嗅探,大量的交互会导致总线带宽达到峰值。因此不要滥用volatile,可以用锁来替代,看场景啦~
四、volatile为什么不保证原子性吗?
public class VolatileAtomicity {
public static volatile int number = 0;
public static void increase() {
number++;
}
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
increase();
}
}, String.valueOf(i)).start();
}
// 当所有累加线程都结束
while(Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(number);
}
}
分析一下increase()方法,通过反编译工具javap得到如下汇编代码:
public static void increase();
Code:
0: getstatic #2 // Field number:I
3: iconst_1
4: iadd
5: putstatic #2 // Field number:I
8: return
number++其实执行了3条指令
:
getstatic:拿number的原始值
iadd:进行加1操作
putfield:把加1后的值写回
执行了getstatic指令number的值取到操作栈顶时,volatile关键字保证了number的值在此时是正确的,但是在执行iconst_1、iadd这些指令的时候,其他线程可能已经把number的值改变了,而操作栈顶的值就变成了过期的数据,所以putstatic指令执行后就可能把较小的number值同步回主内存之中。
总结如下:
在执行number++这行代码时,即使使用volatile修饰number变量,在执行期间还是有可能被其他线程修改。因为在入栈和自增计算执行过程中,该变量有可能正在被其他线程修改,最后计算出来的结果照样存在问题,因此volatile并不能保证非原子操作的原子性,仅在单次读或者单次写这样的原子操作中,volatile能够实现线程安全。
五、禁止指令重排又是啥?
说到指令重排就得知道为什么要重排,有哪几种重排。
会不会感觉到重排把指令顺序都打乱了,这样好吗?
可以回想下小学时候的数学题:2+3-5=?
,如果把运算顺序改为3-5+2=?
,结果也是一样的。所以指令重排是要保证单线程下程序结果不变的情况下做重排。
为什么要重排
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。
有哪几种重排
-
1.编译器优化重排:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
-
2.指令级的并行重排:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
-
3.内存系统的重排:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
注意:
-
单线程环境里面确保最终执行结果和代码顺序的结果一致
-
处理器在进行重排序时,必须要考虑指令之间的
数据依赖性
-
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
volatile怎么实现禁止指令重排?
原理:在volatile生成的指令序列前后插入内存屏障
(Memory Barries)来禁止处理器重排序。
四种内存屏障
屏障类型 | 指令类型 | 说明 |
LoadLoad | Load1; LoadLoad; Load2 | 在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。 |
StoreStore | Store1; StoreStore; Store2 | 在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。 |
LoadStore | Load1; LoadStore; Store2 | 在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。 |
StoreLoad | Store1; StoreLoad; Load2 | 在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个”全能型屏障” ,兼具其它三种内存屏障的功能 |
volatile写的场景如何插入内存屏障
-
在每个volatile写操作的前面插入一个StoreStore屏障(写-写 屏障)。
-
在每个volatile写操作的后面插入一个StoreLoad屏障(写-读 屏障)。
volatile写的场景如何插入内存屏障:
StoreStore屏障可以保证在volatile写(flag赋值操作flag=true)之前,其前面的所有普通写(num的赋值操作num=1) 操作已经对任意处理器可见了,保障所有普通写在volatile写之前刷新到主内存。
volatile写的场景如何插入内存屏障
-
在每个volatile读操作的后面插入一个LoadLoad屏障(读-读 屏障)。
-
在每个volatile读操作的后面插入一个LoadStore屏障(读-写 屏障)。
volatile读场景如何插入内存屏障:
LoadStore屏障可以保证其后面的所有普通写(num的赋值操作num=num+5) 操作必须在volatile读(if(flag))之后执行。
更多推荐
所有评论(0)