前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >笔记07 - Java内存模型

笔记07 - Java内存模型

作者头像
码农帮派
发布2021-01-12 14:52:33
发布2021-01-12 14:52:33
42900
代码可运行
举报
文章被收录于专栏:码农帮派码农帮派
运行总次数:0
代码可运行

线程是CPU调度的最小单元,线程中的字节码最终是放到CPU中执行的,CPU执行的时候伴随着数据的读写,在Java中所有的数据都是放在主内存(RAM)中的,这一过程如下所示:

随着CPU技术的发展,CPU的执行速度越来越快,但是内存技术并没有太大的改变,这就导致内存中数据的读写速度和CPU处理数据的速度差距越来越大,CPU需要较长时间等待内存的读写,这就意味着CPU会出现空转的情况。为了解决性能的瓶颈,进一步释放CPU的运算能力,在CPU中添加了高速缓存(cache)作为数据的缓冲。

在执行任务之前,CPU会首先将数据从主内存中复制到高速缓存中,让运算能够快速进行,当运算完成之后,再将缓存中的结果刷回到主内存中,这样CPU就不用等待主内存中数据的读写了。

目前市面上有的手机有多个CPU、一些CPU还有多核,每个CPU都可以运行一个线程,这就意味着主内存中的数据同时可能被多个线程同时读写,而CPU的高速缓存也是相互独立的,这就会导致主内存中数据的不一致的问题。

另外来自于硬件的指令重排也会导致数据的不一致。CPU内部的运算单元为了尽量被充分利用,处理器会对字节码进行指令重排。

比如下面的代码(b的赋值不影响a的运算):

编译之后的字节码为:

上面的指令中可以看到,指令7(对应最后a + 1)并不影响指令2和指令3,这种情况下,CPU会对指令的顺序进行调整:

从Java语言的角度,调整后的代码顺序:

上面的指令重排在多线程的情况下,由于指令的重拍,单个线程内并没有影响,但可能影响多线程中数据的读写操作,从而导致一些意想不到的结果。

Java的内存模型

如果任由CPU进行优化或多线程的操作,会导致Java程序运行结果出乎意料,为了解决这种问题,JVM推出了Java内存模型来解决。

在java内存模型中,我们统一使用工作内存(working memory)来当作CPU中的寄存器或高速缓存的抽象。线程之间的共享变量存储在主内存(main memory)中,每个线程有自己的工作内存,线程的工作内存中存储了共享内存中变量的副本。

在JMM规范中,又一个重要的规则happens-before。

happens-before先行发生原则

happens-before用于描述两个操作在内存中的可见性,通过保证数据的可见性,从而让应用程序免于数据竞争的干扰。

会发生指令重排的情况:

下面的代码:

代码语言:javascript
代码运行次数:0
运行
复制
int a = 10;  // 1
b = b + 1;   // 2

由于操作2和操作1之间并不会相互影响,这种情况下CPU为了提高计算单元的利用率,一般会进行指令重排。

但是我们要是把代码改成下面这种:

代码语言:javascript
代码运行次数:0
运行
复制
int a = 10;  // 1
b = b + a;   // 2

由于操作2依赖操作1的执行,这种情况下就不会发生指令重排了。

在Java内存模型(JMM)中,有以下的一些情况会自动符合happens-before规则:

1. 程序次序规则

在单线程中,一段代码中逻辑顺序靠前的字节码一定是对后续逻辑字节码可见的。

2. 锁定规则

无论是单线程还是多线程环境中,一个锁处于锁定状态,那么必须首先执行unlock才做,这个所才能被其他的线程获得并重新lock。

3. 变量可见规则

volatile关键字保证了变量的可见性。如果一个线程写了volatile的变量,另外一个线程读取这个变量,那么这个写操作一定是happens-before读操作的。

4. 线程启动规则

Thread对象的start方法先行发生于此线程的每一个动作。假设线程A在执行的过程中通过执行ThreadB.start()来启动线程B,那么线程A中对共享变量的修改,在线程B开始执行后对线程B可见。

5. 线程终结规则

假如线程A在执行的过程中,通过调用ThreadB.join()方法等待线程B终止,那么线程B在终止之前对共享变量的修改在线程A等到返回之后可见。

6. 对象终结规则

一个对象的初始化完成发生在它的finalize()方法之前,也就是对象初始化的数据对它的finalize方法可见。

两种happens-before化的方式

1. 使用volatile关键字

2. 使用synchronized关键字

通过上面两种方式,在一个线程中调用setValue设置的value对其他的线程可见,再setValue之后,其他的线程调用getValue获取到的value一定是1.

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-01-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码农帮派 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档