java 共享内存并行,Java多线程系列(三) - JMM内存模型

本文深入解析了Java内存模型如何影响线程安全,探讨了内存一致性问题、指令重排序以及happens-before规则。重点讲解了共享变量、工作内存、重排序类型及其对编程的影响,以及如何通过happens-before规则确保跨线程可见性。
摘要由CSDN通过智能技术生成

前言

在前面的文章中总结了线程的 状态转换 和一些 基本操作。然而在多线程编程中,稍微不注意就会出现 线程安全 问题,本文会结合 Java 内存模型对出现 线程安全 的原因进行阐述。

正文

1. JMM的介绍

当多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替运行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获取正确的结果,那这个对象是线程安全的。

出现线程安全的问题一般是因为 主内存 和 工作内存 的 数据不一致性 和指令重排序 导致的,而解决 线程安全 的问题最重要的就是理解这两种问题是怎么来的。那么,理解它们的核心在于理解 Java 内存模型( JMM)。

在多线程条件下,多个线程肯定会 相互协作 完成一件事情,一般来说就会涉及到多个线程间相互通信,告知彼此的状态以及当前的执行结果等等。另外为了性能优化,运行时还会进行 编译器指令重排序 和 处理器指令重排序。

2. 内存模型抽象结构

在并发编程中主要需要解决两个问题:线程之间如何进行通信

线程之间如何完成同步

通信是指线程之间以何种机制来 交换信息,主要有 共享内存 和 消息传递两种。 Java 内存模型是基于 共享内存 实现的并发模型,线程之间主要通过 读-写共享变量 来完成隐式通信。

2.1. 共享变量

在 Java 中所有的 实例域,静态域 和 数组元素 都是放在 堆内存 中(所有线程均可访问到,是可以共享的)。而 局部变量,方法定义参数 和 异常处理器参数 不会在线程间共享。共享数据会出现线程安全的问题,而 非共享数据 不会出现 线程安全 的问题。

2.2. JMM抽象结构模型

我们知道 CPU 的 处理速度 和 主存 的 读写速度 不是一个量级的,为了平衡这种巨大的差距,每个 CPU 都会有 高速缓存。因此,共享变量 会先放在 主存 中。

每个线程 都有属于自己的 工作内存,并且会把位于 主存 中的 共享变量拷贝到自己的 工作内存,之后的 读写操作 均使用位于 工作内存 的 变量副本,直到某个时刻将 工作内存 的 变量副本写回到 主存 中去。这就是 JMM 的定义,它决定了一个线程对 共享变量 的 写入 何时对 其他线程是可见的。

d027824cb51303ef54d2849bc1d0dd57.png

上图是 JMM 的抽象示意图,线程A和线程B之间完成通信需要经历如下两步:线程A从主内存中将共享变量读入线程A的工作内存后并进行操作,之后将数据重新写回到主内存中;

线程B从主存中读取最新的共享变量。

从横向去看看,线程A和线程B就好像通过 共享变量 在进行 隐式通信。但是如果线程A更新后数据并没有及时写回到 主存,而此时线程B读到的是过期的 数据,这就出现了 “脏读” 现象。可以通过 同步机制(控制不同线程间操作发生的相对顺序)来解决,或者通过 volatile 关键字,使得每次 volatile 变量都能够 强制刷新 到 主存,从而对 每个线程 都是 可见的。

3. 重排序

一个好的 内存模型,实际上会放松对 处理器 和 编译器 规则的束缚。为了尽可能的提高 并行度, JMM 对尽量减少对底层的约束,使其能够发挥自身优势。因此,在执行程序时,为了提高性能,编译器 和 处理器 常常会对指令进行 重排序。一般重排序可以分为如下三种:

0dd0d7431f451322c86bc90dc1a7bfb9.png

3.1. 编译器优化的重排序

编译器在不改变 单线程程序 语义的前提下,可以重新安排 语句 的 执行顺序。

3.2. 指令级并行的重排序

现代处理器采用了 指令级并行 技术来将 多条指令 重叠执行。如果不存在数据依赖性,处理器可以改变语句对应 机器指令 的 执行顺序。

3.3. 内存系统的重排序

由于 处理器 使用 缓存 和 读/写缓冲区,这使得 加载 和 存储操作 看上去可能是在 乱序执行的。

这些 重排序 会导致 线程安全 的问题,一个很经典的例子就是 DCL 问题。对于编译器重排序, JMM 的 编译器重排序规则 会禁止一些 特定类型的编译器重排序。

对于处理器重排序,编译器在生成 指令序列 的时候会通过 插入内存屏障指令 来禁止某些 特殊 的处理器重排序。

3.4. 数据依赖性

那么什么情况下,不能进行重排序了?下面就来说说数据依赖性,看如下代码:doublepi=3.14//A

doubler=1.0//B

doublearea=pi*r*r//C

这是一个计算圆面积的代码,由于A, B之间没有任何关系,对最终结果也不会存在关系,它们之间执行顺序可以重排序。因此可以执行顺序可以是 A->B->C 或者 B->A->C 执行最终结果都是 3.14,即A和B之间没有数据依赖性。

如果 两个操作 访问 同一个变量,且这两个操作有一个为 写操作,此时这两个操作就存在 数据依赖性。这里就存在三种情况:先读后写

先写后写

先写后读

这三种操作都存在 数据依赖性,如果 重排序 会对最终执行结果造成影响。编译器 和 处理器 在 重排序 时,会遵守 数据依赖性,编译器 和 处理器 不会改变存在数据依赖性关系的两个操作的执行顺序。

3.5. as-if-serial语义

as-if-serial 语义指的是不管怎么 重排序(编译器 和 处理器 为了提供并行度),单线程程序 的执行结果不能被改变。编译器, runtime 和 处理器 都必须遵守 as-if-serial 语义。 as-if-serial 语义把 单线程程序 保护了起来。

比如上面计算圆面积的代码,在 单线程 中,会让人感觉代码是一行一行顺序执行,实际上A, B两行不存在 数据依赖性 可能会进行 重排序,即A, B不是顺序执行的。 as-if-serial 语义使开发人员不必担心 单线程 中 重排序 的问题,也无需担心 内存可见性 问题。

4. happens-before规则

上面的内容讲述了重排序原则,一会是 编译器重排序,一会是 处理器重排序,如果让开发人员去了解这些底层的实现以及具体规则,那么程序员的负担就太重了,严重影响了并发编程的效率。因此, JMM 在上层为开发人员提供了 六条规则,这样我们就可以根据规则去推论跨线程的内存可见性问题,而不用再去理解底层重排序的规则。下面以两个方面来说。

4.1 happens-before定义

JMM 可以通过 happens-before 关系向开发人员提供 跨线程 的 内存可见性 保证, happens-before 原则定义如下:如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。

两个操作之间存在 happens-before 关系,并不意味着 JMM 的具体实现必须要按照 happens-before 关系指定的顺序来执行。如果 重排序 之后的执行结果,与按 happens-before 关系来执行的结果一致,那么这种 重排序 并不非法。

下面来比较一下 as-if-serial 和 happens-before:as-if-serial 语义保证 单线程 内程序的执行结果不被改变, happens-before 保证 多线程 程序的执行结果不被改变。

as-if-serial 语义和 happens-before 这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的 并行度。

4.2. 具体规则程序顺序规则:一个线程 中的每个操作, happens-before 于该线程中的 任意后续操作。

监视器锁规则:对一个锁的解锁, happens-before 于随后对这个锁的加锁。

volatile变量规则:对一个 volatile 域的 写操作, happens-before 于任意后续对这个 volatile 域的 读操作。

传递性:如果A happens-before B,且B happens-before C,那么A happens-beforeC。

start()规则:如果线程 A 执行操作 ThreadB.start() 来启动线程 B,那么 A 线程的 ThreadB.start() 操作 happens-before 于线程 B 中的任意操作。

join()规则:如果线程 A 执行操作 ThreadB.join() 并成功返回,那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join() 操作成功返回为止。

程序中断规则:对线程 interrupted() 方法的调用 先行于 被中断线程的代码检测到中断时间的发生。

对象finalize规则:一个对象的 初始化完成(构造函数执行结束)先行于发生它的 finalize() 方法的开始。

5. 总结

一个 happens-before 规则对应于一个或多个 编译器 和 处理器 的 重排序规则。对于开发人员而言, happens-before 规则简单易懂,利用 JMM提供的内存可见性,开发人员避免了去学习复杂的 重排序规则 以及这些规则的具体实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值