做者 某人Valar
如需转载请保留原文连接html部分图片来自百度,若有侵权请联系删除java
相关推荐:c++
Java多线程之volatilegithub
目录:面试
volatile中文意为挥发物,不稳定的。在Java中也是一个关键字,用于修饰变量。算法
在JMM(Java Memory Model,Java内存模型)中,有main memory,每一个线程也有本身的memory (例如寄存器)。为了性能,一个线程会在本身的memory中保持要访问的变量的副本。缓存
这样就会出现同一个变量在某个瞬间,在一个线程的memory中的值可能与另一个线程memory中的值,或者main memory中的值不一致的状况。安全
一个变量声明为volatile,就意味着这个变量是随时会被其余线程修改,线程在每次使用变量的时候,都会读取变量修改后的最新值。bash
Java内存模型图:
![]()
volatile
不管是修饰实例变量仍是静态变量,都须要放在数据类型
关键字以前,即放在String
、int
等以前。volatile
和final
不能同时修饰一个变量。volatile 是保证变量被写时其结果其余线程可见,而final已经让该变量不能被再次写了。关于原子性、可见性和有序性的介绍,以前的一篇文章有了介绍,传送门
不能。
例如咱们常碰到的i++的问题。
i = 1; //原子性操做,不用使用volatile也不会出现线程安全问题。
复制代码
volatile int i = 0;
i++; //非原子性操做
复制代码
若是咱们开启200个线程并发执行i++
这行代码,每一个线程中只执行一遍。若是volatile能够保证原子性的话,那么i的最终结果应该是200;而实际上咱们发现这个值是会小于200的,缘由是什么呢?
// i++ 其能够被拆解为
一、线程读取i
二、temp = i + 1
三、i = temp
复制代码
temp = i + 1
的操做, 要注意,此时的 i 的值尚未变化,而后B线程也执行了temp = i + 1
的操做,注意,此时A,B两个线程保存的 i 的值都是5,temp 的值都是6i = temp
(6)的操做,此时i的值会当即刷新到主存并通知其余线程保存的 i 值失效, 此时B线程须要从新读取 i 的值那么此时B线程保存的 i 就是6i=temp
(6),因此致使了计算结果比预期少了1。那么如何保证i++这种操做的线程安全呢?
synchronized
关键字或者Lock
。至于为何,能够看下synchronized与原子性synchronized(object){
i++;
}
复制代码
java.util.concurrent.atomic.AtomicInteger
,它使用的是CAS(compare and swap,比较并替换)算法,效率优于第 1 种。volatile关键字的变量写操做时,强制缓存和主存同步,其余线程读时候发现缓存失效,就去读主存,由此保证了变量的可见性。
volatile能够禁止指令重排序,因此说其是能够保证有序性的。
什么是指令重排序(Instruction Reorder)?
在Java内存模型中,容许编译器和处理器对指令进行重排序,重排序的结果不会影响到单线程的执行,但不能保证多线程并发执行时不受影响。
例如如下代码在未发生指令重排序时,其执行顺序为1->2->3->4。但在真正执行时,将可能变为1->2->4->3或者2->1->3->4或者其余。但其会保证1处于3以前,2处于4以前。全部最终结果都是
a=10; b=20
。int a = 0;//语句1 int b = 1;//语句2 a = 10; //语句3 b = 20; //语句4 复制代码
但若是是多线程状况下,另外一个线程中有如下程序。当上述的执行顺序被重排序为1->2->4->3,当线程1执行到第3步
b=20
时,切换到线程2执行,其会输出a此时已是10了
,而此时a的值其实仍是为0。if(b == 20){ System.out.print("a此时已是10了"); } 复制代码
内存屏障
。内存屏障(英语:Memory barrier),也称内存栅栏,内存栅障,屏障指令等,其是一种CPU指令,因此像Java、c++、c语言都有此概念。
A memory barrier, also known as a membar, memory fence or fence instruction, is a type of barrier instruction that causes a CPU or compiler to enforce an ordering constraint on memory operations issued before and after the barrier instruction. This typically means that operations issued prior to the barrier are guaranteed to be performed before operations issued after the barrier. ——— 维基百科
//抽象场景:
Load1;
LoadLoad;
Load2
复制代码
Load1 和 Load2 表明两条读取指令。在Load2要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
//抽象场景:
Store1;
StoreStore;
Store2
复制代码
Store1 和 Store2表明两条写入指令。在Store2写入执行前,保证Store1的写入操做对其它处理器可见
//抽象场景:
Load1;
LoadStore;
Store2
复制代码
在Store2被写入前,保证Load1要读取的数据被读取完毕。
//抽象场景:
Store1;
StoreLoad;
Load2
复制代码
在Load2读取操做执行前,保证Store1的写入对全部处理器可见。StoreLoad屏障的开销是四种屏障中最大的。
在一个变量被volatile修饰后,JVM会为咱们作两件事:
仍是使用上面的例子:
此次使用volatile修饰变量b
int a = 0;//语句1
volatile int b = 1;//语句2
//在线程1中执行的语句
a = 10; //语句3
b = 20; //语句4
//在线程2中执行的语句
if(b == 20){
System.out.print("a此时已是10了");
}
复制代码
在编译以后线程1中的语句将相似于
a = 10; //语句3
----------- StoreStore屏障 ---------------
b = 20; //语句4
----------- StoreLoad屏障 ---------------
复制代码
因为屏障的存在,语句3
和语句4
将没法被指令重排序,从而能够保证在b=20时,a已经被赋值为10了。那么这个程序也就不存在线程安全问题了。
内存屏障阻碍了CPU采用优化技术来下降内存操做延迟,必须考虑所以带来的性能损失。为了达到最佳性能,最好是把要解决的问题模块化,这样处理器能够按单元执行任务,而后在任务单元的边界放上全部须要的内存屏障。采用这个方法可让处理器不受限的执行一个任务单元。
要知道volatile是如何保证可见性的须要先了解下有关CPU缓存的概念。
咱们知道CPU的运算速度要比内存的读写速度快不少,这就形成了内存没法跟上CPU的状况,由此出现了CPU缓存。其是CPU与内存之间的临时数据交换器,咱们常见的CPU会有3级缓存,常称为L一、L二、L3。
下图是Intel Core i7处理器的高速缓存概念模型(图片来自《深刻理解计算机系统》)
当系统运行时,CPU执行计算的过程以下:
在上述的缓存模型下,当多核并发执行某项任务时就容易出现问题。eg.
为了解决这类问题,出现了针对CPU的MESI协议。
在早期的CPU中,是经过在总线加LOCK#锁的方式实现的(又称总线锁)。当一个CPU对其缓存中的数据进行操做的时候,往总线中发送一个Lock信号。 这个时候,全部CPU收到这个信号以后就不操做本身缓存中的对应数据了,当操做结束,释放锁之后,全部的CPU就去内存中获取最新数据更新。
但这种方式开销太大,因此Intel开发了缓存一致性协议,也就是MESI协议。它的方法是在CPU缓存中保存一个标记位,这个标记位有四种状态:
CPU的读取遵循下面几点:
举个常见的例子就是:
当CPU写数据时,若是发现操做的变量是共享变量,即在其余CPU中也存在该变量的副本,那么他会发出信号通知其余CPU将该变量的缓存行设置为无效状态。当其余CPU使用这个变量时,首先会去嗅探是否有对该变量更改的信号,当发现这个变量的缓存行已经无效时,会重新从内存中读取这个变量。
了解了上面的内容,就能够很容易的理解volatile是如何实现的了。
参考:
volatile到此也介绍的很多了,最后来讲下其与synchronized的区别。
了解更多synchronized的相关内容,请戳这里。
当你和面试官说到这里时,你最好清楚里面的具体细节,例如是从何种角度来看的有序性,以及如何实现的该特性,否则面试官很容易被问住的。
至此关于volatile的内容到这里就结束了,若是文中有错误的地方、或者有其余关于volatile
比较重要的内容又没有介绍到的,欢迎在评论区里留言,一块儿交流学习。