前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从JVM内存模型来看并发编程中的可见性和有序性

从JVM内存模型来看并发编程中的可见性和有序性

作者头像
35岁程序员那些事
发布2022-09-23 17:26:30
3280
发布2022-09-23 17:26:30
举报

0 目录

1 什么是JVM内存模型

2 Happens-Before规则

2.1 程序的顺序性规则

2.2 volatile 变量规则

2.3 传递性

2.4 管程中锁的规则

2.5 线程start()规则

2.6 线程join()规则

3 总结

工作做螺丝钉,面试造火箭,我想这个是每个程序员比较头疼的事情,但是又有必须经历的流程,我们再聊聊从JVM内存模型来看并发编程中的可见性和有序性。

1 什么是JVM内存模型

我们都已经知道,导致可见性的原因是缓存,导致有序性的原因是编译优化,那解决可见性、有序性最直接的办法就是禁用缓存和编译优化,但是这样问题虽然解决了,咱们程序的性能可就堪忧了。合理的方案应该是按需禁用缓存以及编译优化。

软件开发人员如何做到“按需禁用”呢?对于并发程序,何时禁用缓存以及编译优化只有软件开发人员知道,那所谓“按需禁用”其实就是指按照软件开发人员的要求来禁用。所以,为了解决可见性和有序性问题,只需要提供给软件开发人员按需禁用缓存和编译优化的方法即可。

Java 内存模型是个很复杂的规范,可以从不同的视角来解读,站在软件开发人员的视角,本质上可以理解为,Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。

总的来说,这些方法包括 volatile、synchronized 和 final 三个关键字

其中最核心的部分,还是要了解Java 的Happens-Before规则。

2 Happens-Before规则

软件开发人员要如何理解 Happens-Before 呢?其实Happens-Before 并不是说前面一个操作发生在后续操作的前面,它真正要表达的是:前面一个操作的结果对后续操作是可见的。Happens-Before 规则就是要保证线程之间的一种“心灵感应”。所以比较正式的说法是:Happens-Before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 Happens-Before 规则。

Happens-Before 规则总共包括如下六条规则:

2.1 程序的顺序性规则

这条规则是指在一个线程中,按照程序顺序,前面的操作 Happens-Before 于后面的任意操作。这个是非常符合单线程里面的思维:程序前面对某个变量的修改一定是对后续操作可见的。这条规则在顺序编程中是通用的,但是如果进入并发编程的领域就行不通了。

2.2 volatile 变量规则

这条规则是指对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作。这个就有点费解了,对一个 volatile 变量的写操作相对于后续对这个 volatile 变量的读操作可见,这怎么看都是禁用缓存的意思啊,貌似和 1.5 版本以前的语义没有变化啊?如果单看这个规则,的确是这样,但是如果我们关联一下规则 3,就有点不一样的感觉了。

我之前也是搞混了 volatile 变量的语义,错误的认为“如果我将变量声明为volatile 类型,就是禁用了CPU缓存,会影响性能”,经过查询相关文档之后,才认识到自己的错误。

2.3 传递性

这条规则是指如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。

2.4 管程中锁的规则

这条规则是指对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。要理解这个规则,就首先我们需要理解“管程指的是什么”。

管程是一种通用的同步语义,在 Java 中指的就是 synchronized,synchronized 是 Java 里对管程的实现。

管程中的锁在 Java 里是隐式实现的,使用synchronized关键字之后,代码进入同步块之前,会自动加锁,而在代码块执行完会自动释放锁,加锁以及释放锁都是编译器帮我们实现的。

2.5 线程start()规则

这条是关于线程启动的。它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。换句话说就是,如果线程 A 调用线程 B 的 start() 方法(即在线程 A 中启动线程 B),那么该 start() 操作 Happens-Before 于线程 B 中的任意操作。

2.6 线程join()规则

这条是关于线程等待的。它是指主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能够看到子线程的操作。当然所谓的“看到”,指的是对共享变量的操作。换句话说就是,如果在线程 A 中,调用线程 B 的 join() 并成功返回,那么线程 B 中的任意操作 Happens-Before 于该 join() 操作的返回。

好吧我们再来回头看看final关键字

前面已经描述过 volatile 为的是禁用缓存以及编译优化,我们再从另外一个方面来看,有没有办法告诉编译器优化得更好一点呢?这个可以有,就是 final 关键字。

final 修饰变量时,初衷是告诉编译器:这个变量生而不变,可以可劲儿优化。

Java 编译器在 JDK1.5 以前的版本的确优化得很努力,以至于都优化错了。问题类似于上一期提到的利用双重检查方法创建单例,构造函数的错误重排导致线程可能看到 final 变量的值会变化。

当然了,在 JDK1.5 以后 Java 内存模型对 final 类型变量的重排进行了约束。现在只要我们提供正确构造函数没有“逸出”,就不会出问题了。“逸出”一般是指软件开发人员通过方法入参将全局变量传递到方法中,而这个全局对象又会被多个线程共享,导致逸出的现象。

3 总结

Java 的内存模型是并发编程领域的一次重要创新,之后 C++、C#、Golang 等高级语言都开始支持内存模型。

Java 内存模型里面,最晦涩的部分就是 Happens-Before 规则了。

在 Java 语言里面,Happens-Before 的语义本质上是一种可见性。

A Happens-Before B 意味着 A 事件对 B 事件来说是可见的,无论 A 事件和 B 事件是否发生在同一个线程里。例如 A 事件发生在线程 1 上,B 事件发生在线程 2 上,Happens-Before 规则保证线程 2 上也能看到 A 事件的发生。

Java 内存模型主要分为两部分,一部分面向应用开发人员,另一部分是面向 JVM 的实现人员的,我们可以重点关注前者,也就是和编写并发程序相关的部分,这部分内容的核心就是 Happens-Before 规则,当然只是从方法论的角度去分析了Happens-Before 规则,如果要融会贯通,还是需要大量的实战。

以上都是本人对Happens-Before 规则的个人理解,如有不对的地方,欢迎留言。

知识输出是笔者的初衷,借助知识输出,能够认识更多的牛人,能够和牛人沟通,也是自己技术提升的一个机会。


下一期:开启35岁程序员高并发认知系列


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

本文分享自 架构随笔录 微信公众号,前往查看

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

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

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