java内存模型 知乎_内存可见性,指令重排序,JIT。。。。。。从一个知乎问题谈起...

问题是这样的:

package com.cc.test.volatileTest;

public class VolatileBarrierExample {

private static boolean stop = false;

public static void main(String[] args) throws InterruptedException {

Thread thread = new Thread(new Runnable() {

@Override

public void run() {

while (!stop) {

}

}

});

thread.start();

Thread.sleep(1000);

stop = true;

thread.join();

}

}

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

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

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

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

package com.cc.test.volatileTest;

public class VolatileBarrierExample {

private static boolean stop = false;

private static volatileint i = 0;

public static void main(String[] args) throws InterruptedException {

Thread thread = new Thread(new Runnable() {

@Override

public void run() {

int i = 0;

while (!stop) {

i++; }

}

});

thread.start();

Thread.sleep(1000);

stop = true;

thread.join();

}

}

这段程序是可以在运行一秒后结束的,也就是说子线程对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编译后的结果)

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

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

7e10ead10b8dc9a76d41431324b51d8f.png

在第一个红框处检测stop变量,如果为true,那么跳转到L0001处继续执行(L0001处再往下走函数就退出了),但此时stop为false,所以不会走这个分支

L0000,inc %ebp。也就是i++

test %eax, -0x239864a(%rip),轮询SAFEPOINT的操作,可以无视

jmp L0000,无条件跳转回L0000处继续执行i++

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

if(!stop){

while(true){

i++;

}

}

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

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

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

595d375916b4c9d75cbd508123507dc7.png

从第一个红框开始看:

jmp L0001,无条件跳转到label L0001处

movzbl 0x6c(%r10),%r8d; 访问static变量stop,并将其复制到寄存器r8d里

test %r8d, %r8d; je L0000; 如果r8d里的值为0,跳转到L0000处,否则继续往下走(函数结束)

L000: mov 0x68(%r10), %r8d; 访问static变量i,并将其复制到寄存器r8d里

inc %r8d; 自增r8d里的值

mov %r8d, 0x68(%r10); 将自增后r8d里的新值复制回static变量i中(上面三行是i++的流程)

lock addl $0x0, (%rsp); 给rsp寄存器里的值加0,没有任何效果,关键在于前面的lock前缀,会导致cache line的刷新,从而实现变量i的volatile语义

test %eax, -0x242a056(%rip); 轮询SAFEPOINT的操作,可以无视

L0001,回到step 2

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

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

总结

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

volotile关键字的内存可见性及重排序

在理解volotile关键字的作用之前,先粗略解释下内存可见性与指令重排序. 1. 内存可见性 Java内存模型规定,对于多个线程共享的变量,存储在主内存当中,每个线程都有自己独立的工作内存,并且线程 ...

原子性、内存可见性和重排序——重新认识synchronized和volatile

一.原子性 原子性操作指相应的操作是单一不可分割的操作.例如,对int变量count执行count++d操作就不是原子性操作.因为count++实际上可以分解为3个操作:(1)读取变量count的当前 ...

Java内存模型(三)原子性、内存可见性、重排序、顺序一致性、volatile、锁、final

一.原子性 原子性操作指相应的操作是单一不可分割的操作.例如,对int变量count执行count++d操作就不是原子性操作.因为count++实际上可以分解为3个操作:(1)读取变量co ...

java并发学习--第九章 指令重排序

一.happns-before happns-before是学习指令重排序前的一个必须了解的知识点,他的作用主要是就是用来判断代码的执行顺序. 1.定义 happens-before是用来指定两个操作 ...

JVM并发机制的探讨——内存模型、内存可见性和指令重排序

并发本来就是个有意思的问题,尤其是现在又流行这么一句话:“高帅富加机器,穷矮搓搞优化”. 从这句话可以看到,无论是高帅富还是穷矮搓都需要深入理解并发编程,高帅富加多了机器,需要协调多台机器或者多个CP ...

轻松学JVM(二)——内存模型、可见性、指令重排序

上一篇我们介绍了JVM的基本运行流程以及内存结构,对JVM有了初步的认识,这篇文章我们将根据JVM的内存模型探索java当中变量的可见性以及不同的java指令在并发时可能发生的指令重排序的情况. 内存 ...

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

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

深入理解JVM(二)——内存模型、可见性、指令重排序

上一篇我们介绍了JVM的基本运行流程以及内存结构,对JVM有了初步的认识,这篇文章我们将根据JVM的内存模型探索java当中变量的可见性以及不同的java指令在并发时可能发生的指令重排序的情况. 内存 ...

深入理解JVM一内存模型、可见性、指令重排序

一.内存模型 首先我们思考一下一个java线程要向另外一个线程进行通信,应该怎么做,我们再把需求明确一点,一个java线程对一个变量的更新怎么通知到另外一个线程呢?我们知道java当中的实例对象.数组 ...

随机推荐

Grunt备忘录

一.安装Grunt 应在全局环境下安装Grunt,以保障在任何目录下都能够正确找到grunt npm install grunt-cli -g 二.对已有Grunt基本目录结构文件进行操作 Grunt ...

Unity Shader——Writing Surface Shaders(0)

从今天起,开始翻译Unity关于shader的官方文档.翻译水平比较一般,目的主要是通过翻译来提升对shader的见解,也让其他人更容易的了解shader.以下开始正文内容: 编写Surface Sh ...

UVA 10594-Date Flow(无向图的最小费用网络流+题目给的数据有误)

题意:给一个有N个点的无向图,要求从1向N传送一定的数据,每条边的容量是一定的,如果能做到,输出最小的费用,否则输出Impossible. 解析:由于是无向图,所以每个有连接的两个点要建4条边,分别是 ...

TCP三次握手和四次挥手具体解释

三次握手:建立TCP须要三次握手才干建立, 先Client端发送连接请求报文,Server段接受连接后回复ACK报文,并为这次连接分配资源.Client端接收到ACK报文后也向Server段发生ACK ...

linux XAMPP安装与使用

linux安装 下载好后考至目录, chmod +x  接安装包 在执行 ./ 接安装包 启动 XAMPP /opt/lampp/lampp start 停止 XAMPP /opt/lampp/lam ...

centOS 6 服务管理与服务脚本

服务管理与服务脚本   linux服务 服务管理与服务脚本 linux服务 服务启动过程详解 chkconfig命令 非独立服务与xinetd进程 一个特殊的服务脚本   服务启动过程详解 在开机启动 ...

Yarn 入门

Yarn 是快速.可靠.安全的 js 包管理器. 关键词: nodejs, 包管理, yarn 简介 Yarn 是快速.可靠.安全的 js 包管理器. 快速 - Yarn 会缓存它下载的每个包,所以无 ...

iOS开发之--解决 swap file “*.swp”already exists!问题

刚才我在vim导入第三方库的时候,突然发现我把podfile的名字打错了,然后我直接终止了进程,然后你懂得出错了,,E325......., 其实使用vim编辑文件实际上是先copy一份临时文件,病映 ...

SQLSqlserver中如何将一列数据,不重复的拼接成一个字符串

把一列数据拼接成一个字符串比较简单: declare @test varchar(500) set @test=''; select @test=@test+name+',' from person ...

使用Python学习RabbitMQ消息队列

rabbitmq基本管理命令: 一步启动Erlang node和Rabbit应用:sudo rabbitmq-server 在后台启动Rabbit node:sudo rabbitmq-server ...

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值