在知乎上看到一个问题《java中volatile关键字的疑惑?》,引起了我的兴趣

问题是这样的:

 1 package com.cc.test.volatileTest;
 2
 3 public class VolatileBarrierExample {
 4     private static boolean stop = false;
 5
 6     public static void main(String[] args) throws InterruptedException {
 7         Thread thread = new Thread(new Runnable() {
 8             @Override
 9             public void run() {
10                 while (!stop) {
11                 }
12             }
13         });
14
15         thread.start();
16         Thread.sleep(1000);
17         stop = true;
18         thread.join();
19     }
20 }

这段代码的主要目的是:主线程修改非volatile类型的全局变量stop,子线程轮询stop,如果stop发生变动,则程序退出。

但是如果实际运行这段代码会造成死循环,程序无法正常退出。

如果对Java并发编程有一定的基础,应该已经知道这个现象是由于stop变量不是volatile的,主线程对stop的修改不一定能被子线程看到而引起的。

但是题主玩了个花样,额外定义了一个static类型的volatile变量i,在while循环中对i进行自增操作,代码如下所示:

 1 package com.cc.test.volatileTest;
 2
 3 public class VolatileBarrierExample {
 4     private static boolean stop = false;
 5     private static volatile int i = 0;
 6
 7     public static void main(String[] args) throws InterruptedException {
 8         Thread thread = new Thread(new Runnable() {
 9             @Override
10             public void run() {
11                 int i = 0;
12                 while (!stop) {
13                     i++;
14                 }
15             }
16         });
17
18         thread.start();
19         Thread.sleep(1000);
20         stop = true;
21         thread.join();
22     }
23 }

这段程序是可以在运行一秒后结束的,也就是说子线程对volatile类型变量i的读写,使非volatile类型变量stop的修改对于子线程是可见的!

看起来令人感到困惑,但是实际上这个问题是不成立的。

先给出概括性的答案:stop变量的可见性无论在哪种场景中都没有得到保证。这两个场景中程序是否能正常退出,跟JVM实现与CPU架构有关,没有确定性的答案。

下面从两个不同的角度来分析

一:happens-before原则:

第一个场景就不谈了,即使在第二种场景里,虽然子线程中有对volatile类型变量i的读写+非volatile类型变量stop的读,但是主线程中只有对非volatile类型变量stop的写入,因此无法建立 (主线程对stop的写) happens-before于 (子线程对stop的读) 的关系

也就是不能指望主线程对stop的写一定能被子线程看到。

虽然场景二在实际运行时程序依然正确终止了,但是这个只能算是运气好,如果换一种JVM实现或者换一种CPU架构,可能场景二也会陷入死循环。

可以设想这样的一个场景,主/子线程分别在core1/core2上运行,core1的cache中有stop的副本,core2的cache中有stop与i的副本,而且stop和i不在同一条cacheline里。

core1修改了stop变量,但是由于stop不是volatile的,这个改动可以只发生在core1的cache里,而被修改的cacheline理论上可以永远不刷回内存,这样core2上的子线程就永远也看不到stop的变化了。

二:JIT角度:

由于run方法里的while循环会被执行很多次,所以必然会触发jit编译,下面来分析两种情况下jit编译后的结果(触发了多次jit编译,只贴出最后一次C2等级jit编译后的结果)

如何查看JIT后的汇编码请参看我的这篇博文:《如何在windows平台下使用hsdis与jitwatch查看JIT后的汇编码》

ps. 回答首发于知乎,重新截图太麻烦,因此实际分析使用的Java源码与前面贴的代码略有不同,不影响理解,会意即可。

A. i为run方法内的局部变量的情况:

  1. 在第一个红框处检测stop变量,如果为true,那么跳转到L0001处继续执行(L0001处再往下走函数就退出了),但此时stop为false,所以不会走这个分支
  2. L0000,inc %ebp。也就是i++
  3. test %eax, -0x239864a(%rip),轮询SAFEPOINT的操作,可以无视
  4. jmp L0000,无条件跳转回L0000处继续执行i++

如果把jit编译后的代码改写回来,大概是这个样子

1 if(!stop){
2      while(true){
3           i++;
4     }
5 }

非常明显的指令重排序,JVM觉得每次循环都去访问非volatile类型的stop变量太浪费了,就只在函数执行之初访问一次stop,后续无论stop变量怎么变,都不管了。

第一种情况死循环就是这么来的。

B. i为全局的volatile变量的情况:

从第一个红框开始看:

  1. jmp L0001,无条件跳转到label L0001处
  2. movzbl 0x6c(%r10),%r8d; 访问static变量stop,并将其复制到寄存器r8d里
  3. test %r8d, %r8d; je L0000; 如果r8d里的值为0,跳转到L0000处,否则继续往下走(函数结束)
  4. L000: mov 0x68(%r10), %r8d; 访问static变量i,并将其复制到寄存器r8d里
  5. inc %r8d; 自增r8d里的值
  6. mov %r8d, 0x68(%r10); 将自增后r8d里的新值复制回static变量i中(上面三行是i++的流程)
  7. lock addl $0x0, (%rsp); 给rsp寄存器里的值加0,没有任何效果,关键在于前面的lock前缀,会导致cache line的刷新,从而实现变量i的volatile语义
  8. test %eax, -0x242a056(%rip); 轮询SAFEPOINT的操作,可以无视
  9. L0001,回到step 2

也就是说,每次循环都会去访问一次stop变量,最终访问到stop被修改后的新值(但是不能确保在所有JVM与所有CPU架构上都一定能访问到),导致循环结束。

这两种场景的区别主要在于第二种情况的循环中有对static volatile类型变量i的访问,导致jit编译时JVM无法做出激进的优化,是附加的效果。

总结

涉及到内存可见性的问题,一定要用happens-before原则细致分析。因为你很难知道JVM在背后悄悄做了什么奇怪的优化。

转载于:https://www.cnblogs.com/stevenczp/p/7978554.html

内存可见性,指令重排序,JIT。。。。。。从一个知乎问题谈起相关推荐

  1. JVM学习--(二)内存模型、可见性、指令重排序

    我们将根据JVM的内存模型探索java当中变量的可见性以及不同的java指令在并发时可能发生的指令重排序的情况. 内存模型 首先我们思考一下一个java线程要向另外一个线程进行通信,应该怎么做,我们再 ...

  2. 说说Java中原子性,可见性与指令重排序的理解

    原子性:就是读数据,处理数据,写数据 这三个步骤不能被终止,或者打断:就是不能被线程调度器中断,切换线程. 这样,才能保证,原子操作在线程切换,并行处理上保证数据地顺序累加处理. 可见性:是Jvm较为 ...

  3. Java之volatile如何保证可见性和指令重排序

    1 我们先了解CPU缓存 CPU缓存为了解决CPU运算速度与内存读写速度不匹配的问题,因为CPU运算速度要比内存读写速度快得多 一次主内存的访问通常在几十到几百个时钟周期 一次L1高速缓存的读写只需要 ...

  4. Java指令屏障_指令重排序和内存屏障

    sap hana计算技术项目实战指南内存 61元 (需用券) 去购买 > 一.指令重排序 指令重排序分为三种,分别为编译器优化重排序.指令级并行重排序.内存系统重排序.如图所示,后面两种为处理器 ...

  5. 什么是指令重排序和内存屏障,看完你就懂了

    面试官在问到多线程编程的时候,指令重排序.内存屏障经常会被提起.如果你对这两者有一定的理解,那这就是你的加分项. (一)什么是指令重排序 为了使处理器内部的运算单元能尽量被充分利用,处理器可能会对输入 ...

  6. 由Java引起的指令重排序思考

    背景 问题出现 最近遇到了一个NullPointerException,虽然量不大,但是很怪异,大致长这个样子 这是个什么空指针?居然说我LinkedList.iterator().hasNext() ...

  7. java volidate线程安全_03.(多线程与并发)面试题-02--Volidate的原理和指令重排序

    线程栈(线程的工作内存)保存了线程运行时候变量值信息.当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本 ...

  8. java重排序_Java synchronized 能防止指令重排序吗?

    @ZealTalk 说的是 synchronized 可以防止指令重排,这个观点不对的,也欢迎回答的各位来讨论 synchronized 的有序性 来讨论这个问题先,先看看 Java 里的操作无序现象 ...

  9. Java的多线程机制系列:(四)不得不提的volatile及指令重排序(happen-before)

    一.不得不提的volatile volatile是个很老的关键字,几乎伴随着JDK的诞生而诞生,我们都知道这个关键字,但又不太清楚什么时候会使用它:我们在JDK及开源框架中随处可见这个关键字,但并发专 ...

  10. Java并发编程之指令重排序

    在我们面试过程中,通常避免不了会被问到什么是指令重排序?本文就这个问题进行探索. 重排序 前言 一.重排序种类 二.happens-before 三.重排序 1.数据依赖性 2. as-if-seri ...

最新文章

  1. 通过反射获取及调用方法(Method)
  2. MFC中设备描述表dc的使用
  3. MIT开发出新界面系统 操作员可用思维控制机器人
  4. django 1.8 官方文档翻译:2-5-9 条件表达式
  5. MySQL STR_TO_DATE函数
  6. Random Maze HDU - 4067 费用流/可行流
  7. C#中用WMI实现对驱动的查询
  8. idea 用iterm 终端_iTerm2 都不会用,还敢自称老司机?(上)
  9. 少儿图论:八岁小孩眼里的欧拉公式
  10. 如何知道AppDomain租约是否已过期呢
  11. 三次样条插值之三对角矩阵算法
  12. 视频直播源代码,视频文件当中的存储方法
  13. 电子设计竞赛控制组——完整旋转倒立摆程序
  14. 开机netmeeting已删除_NetMeeting
  15. Java实现动态sin和cos函数图像
  16. 蜻蜓飞过,从此智能硬件厂商有了儿童梦工厂
  17. angluarjs+springmvc实现excel上传并解析,对个别字段进行非空校验,txt生成,txt生成的条件为某列必须为某值且只提供固定的几列发送到ftp...
  18. sccm安装linux系统,SCCM Client for LINUX on FIPS Enabled Systems
  19. 《计算机应用基础》课程计划,计算机应用基础课程教学计划
  20. jar包过大?手把手教你分析Maven依赖,完美“瘦身”优化!

热门文章

  1. xml和TreeView
  2. 【转】Linux删除文件未释放空间问题处理
  3. 操,escape sequence的输入方法我以前找过一次,这次又忘了,又找了一次,记下来,...
  4. 所有win7机器都必须要做的一个优化!作用:让系统流畅,减少卡顿
  5. 09.Java数据算法
  6. struts2——快速入门
  7. iOS开发者《用2块钱快速创建你的网站或博客》
  8. Composite模式
  9. 迫切想要成功之后的喜悦感,失败太久有点心灵上小小的打击,还需要继续前进。...
  10. 将服务费用DIY到底----走出软件作坊:三五个人十来条枪 如何成为开发正规军(十)...