标签:缓存 变量 第一个 指令 art opera vol ++操作 incr
知识关联:
告读者:本文中的代码均由jdk1.7运行,可能由于jdk1.8的优化,笔者用jdk1.8测试得不到预想结果。
volatile关键字是基于MESI缓存一致性协议的,协议的主要内容是多个CPU从主存读取数据到缓存,当其中某个CPU修改了缓存中数据,该数据会立刻同步回主存,其他CPU通过总线嗅探机制可以感知到数据的变化,从而将自己缓存中的数据失效,重新从主存中获取。
volatile修饰的实例变量或类变量具备两层语义:
volatile修饰的变量,当一个线程在自己工作内存中执行修改操作,会立即将修改后的值同步回主存(不会等到程序执行结束或者其他时间),其他线程工作内存的的值会立即失效再从主存中获取。
1 public class FlagTest { 2 private volatile static boolean flag = false; 3 4 public static void main(String[] args) throws InterruptedException { 5 new Thread(new Runnable() { 6 @Override 7 public void run() { 8 while (!flag){ 9 10 } 11 } 12 },"minder").start(); 13 14 15 TimeUnit.SECONDS.sleep(2); 16 17 new Thread(new Runnable() { 18 @Override 19 public void run() { 20 System.out.println("the work is done"); 21 flag = true; 22 } 23 },"worker").start(); 24 } 25 } 26
上例,若flag不用volatile修饰,在worker将flag值为true后,程序仍一直处于运行状态。volatile保证了worker线程对flag的每次操作minder线程都能看到(对应于happens-before原则第三条,volatile原则:对一个变量的写操作要早于对这个变量的读操作),具体步骤如下:
1)minder线程从主存中获取flag的值,缓存到工作内存
2)woker线程将工作内存中flag值为true,立即刷新回主存
3)minder线程工作内存中的flag失效,重新到主存中获取flag
(1) 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
(2) 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
volatile保证顺序性的方式,是直接禁止JVM和处理器对volatile关键字修饰的指令重排序,但是对于volatile前后无依赖关系到的指令则可以随便怎么排序。
例1:
1 int x = 0; 2 int y = 0; 3 volatile int z = 10; 4 x++; 5 y++
在语句z=20之前,先执行x还是y的定义赋值没什么关系,只要能保证执行到z=20的时候x=0,y=1,就可以,同理关于x的自增和y的自减操作都必须在z=20以后发生。
例2:
1 ublic class Load { 2 private volatile boolean flag = false; 3 private static Socket socket; 4 5 public Load(){ 6 socket = new Socket(); 7 flag = true; 8 } 9 10 public static void main(String[] args) { 11 final Load[] load = new Load[1]; 12 new Thread(){ 13 @Override 14 public void run() { 15 load[0] = new Load(); 16 } 17 }.start(); 18 19 new Thread(){ 20 @Override 21 public void run() { 22 while (true){ 23 if (load[0].flag){ 24 //operate files and methhods in Load 25 } 26 } 27 } 28 }.start(); 29 } 30 } 31
上例,flag被volatile修饰,意味着在执行到flag=true时,一定再执行且完成了对socket的初始化。因此它能避免多线程情况下,如上,若flag不是volatile,则重排序可能出现两个情况:
1)socket在flag=true后执行,一个线程初始化Load,则到flag=true时,socket还未初始化。另一个线程使用的是一个未初始化完成的Load对象。
2)socket在flag=true之后执行,socket初始化可能还为初始化完成时,flag已经被赋为true。另一个线程使用的是一个未初始化完成的对象。
volatile修饰能保证,socket初始化在flag=true之前进行初始化操作,且在socket初始化完成后再执行flag=true。
1 public class IncreaseTest { 2 private static int p = 0; 3 4 public static void main(String[] args) throws InterruptedException { 5 for (int i = 0;i < 3;i++) 6 new Thread(){ 7 @Override 8 public void run() { 9 for (int i = 0;i < 1000;i++){ 10 p++; 11 System.out.println(Thread.currentThread().getName()+"="+p); 12 } 13 } 14 }.start(); 15 } 16 } 17
上述代码多次运行结果一定是小于等于3000,导致结果的主要原因是p++操作是非原子操作,是由三步组成:
1)从主存中获取p,缓存至当前线程工作内存;
2)在工作内存中进行p+1操作;
3)将最新的值刷新回主存。
上面三个操作简单对应内存交互规则,分别对应于load,修改和store。三个操作单独都是原子性操作,但组合起来就不是,在执行过程中,可能发生:
1)线程A从主存读取到p=10,由于CPU时间片调度关系,执行权换到线程B;
2)因为线程A未修改p,线程B从主存中仍获取到p=10;
3)线程B在工作内存中执行p+1操作,但还未刷新到主存,CPU又将执行权给线程A;
4)线程B未刷新回主存,线程A看不到修改,线程A直接将工作内存中的p执行+1操作;
5)线程A将p=11写入主存;
6)线程B将p=11写入主存。
这样两次操作实际p只进行了一次变化。
问题详解:
第三步,线程A将p=11写入主存后,线程B再次执行,P为什么没有失效,去获取最新的值?
其实严格的说,对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。在《Java并发编程的艺术》中有这一段描述:“在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。”需要注意的是,这里的修改操作,是指的一个操作。
总的来说,在自增操作中,执行load,修改和store三步,在还没有将store指令操作完成的时候,如果有其他线程介入修改共享变量并更新,此时更新是错误的。因为此时内存可见性是针对执行load操作的时候,需要刷新读取最新的值。
volatile保证可见性和顺序性,是通过“lock;”实现的(汇编可见)。“lock;”前缀相当于一个内存屏障,该屏障会为指令的执行提供如下几个保障。
(1) 使用上的区别
(2) 对原子性的保证
(3) 对可见性的保证
(4) 对有序性的保证
(5) 其他
标签:缓存 变量 第一个 指令 art opera vol ++操作 incr
原文地址:https://www.cnblogs.com/privateNotesOfAidanChen/p/12903107.html