并发编程之happensbefore

5次阅读

共计 2790 个字符,预计需要花费 7 分钟才能阅读完成。

前言
Jdk5 开始,Java 使用新的 JSP-133 内存模型,JSR-133 使用 happens-before 的概念来阐述操作直接的内存可见性。在 JMM 中,如果一个操作执行结果需对另一个操作课件,那么这两个操作之间必须要存在 happen-before 关系。

一、文章导图

二、Happens-Before

happens-before 是 JMM 的核心概念,要理解 happens-before,先来看下 JMM(Java Memory Model)的设计意图。

1、JMM 的设计意图

设计 JMM,需要考虑的两个关键因素:
1)、程序员对内存模型的使用。程序员希望内存模型易于理解、易于编程,希望一个强内存模型来编写代码。
2)、编译器和处理器对内存模型的实现。编译器和处理器希望内存模型对它们的束缚越少越好,这样它们就可能做更多的优化来提高性能,希望实现一个弱内存模型。

两个因素相互矛盾,如何找到一个平衡点呢?

设计 JMM 的核心目的:
一方面,为程序员提供足够强的内存可见性保证;另一方面,对编译器和处理器的限制尽可能放松。

设计 JMM 的策略:对于一段程序
1)对于会改变程序的执行结果的重排序,JMM 要求编译器和处理器禁止这种排序
2)对于不会改变程序的执行结果的重排序,JMM 对编译器和处理器不做要求

2、Happens-before 介绍

《JSR-133:Java Memory Model and Thread Specification》对 happens-before 关系的定义如下:
1)、如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见。
2)、两个操作之间存在 happens-before 关系,如果重排序后的执行结果与按 happens-before 关系指定的顺序执行结果一致,这种重排序是被允许的。
上面第 1 条是对程序员的承诺,第 2 条是对编译器和处理器重排序的约束原则。

JMM 遵循的的一个原则是:只要不改变程序的执行结果,编译器和处理器怎么优化都可以。JMM 这么做的原因是程序员对于是否进行重排序等并不关心,关心的是执行结果。

3、Happens-before 规则

《JSR-133:Java Memory Model and Thread Specification》定义了如下的 happens-before 原则。
1)、程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。
2)、监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁。
3)、volatile 变量规则:对于一个 volatile 域的写,happens-before 于任意后续对这个变量的读。
4)、传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
5)、start() 规则:如果线程 A 执行操作 ThreadB.start()(启动线程 B),那么 A 线程的 ThreadB.start() 操作 happens-before 于线程 B 中的任意操作。
6)、join() 规则:如果线程 A 执行操作 ThreadB.join() 并成功,那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join() 操作成功返回。

如下示例说明:

如图:按程序顺序规则知,1 happens-before 2;3 happens-before 4;按 volatile 变量规则知,2 happens-before 3;再有传递性可知 1 happens-before 4。

那么,什么是重排序呢?为什么要重排序呢?

三、重排序

重排序:是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的手段。

处理器为啥要重排序呢?
因为一条指令可能会涉及到很多步骤,而每个步骤可能会用到不同的寄存器。CPU 使用了流水先的方式进行处理,CPU 有多个功能单元(如获取、解码、运算等),一条指令也分为多个单元,那么第一条指令执行还没完毕,就有可能执行第二条指令,前提是这两条指令功能单元相同或相似,所以可以通过重排序的方式使得功能单元相似的指令连接执行,来减少流水线中断的情况。
比如说:
执行方式 1

int x = 1;
int y = 2;
x = x + 1;
y = y + 1;

执行方式 2

int x = 1;
x = x + 1;
int y = 2;
y = y + 1;

性能方面:执行方式 2 可能比执行方式 1 好点,因为执行方式 2 中 x 或 y 已经在寄存器中了,获取和运算会连续执行。

四、锁获取 - 释放建立的 happens-before 关系

1、happens-before 关系

锁是 Java 并发编程中最重要的同步机制。锁除了让临界区互斥执行外,还可以让释放锁的线程想获取同一个锁的线程发送消息。
如下锁释放 - 获取的示例代码:
`public class MonitorExample {

int a = 0;

public synchronized void writer() { // 1
    a++;                            // 2
}                                   // 3

private synchronized void reader() {   // 4
    int i = a;                         // 5
    System.out.println(i);             // 6
}

}
`
如果线程 A 执行 writer() 方法,随后线程 B 执行 reader() 方法。其包含的 happens-before 规则有:
1)、程序顺序执行:1 happens-before 2,2 happens-before 3;4 happens-before 5,5 happens-before 6。
2)、根据监视器锁规则:3 happens-before 4。
3)、结合传递性:2 happens-before 5。
因此,线程 A 在释放锁之前的对所以共享变量的操作,在线程 B 获取该锁后对共享变量立即可见。

2、内存语义

如上示例,当线程 A 释放锁时,JMM 会把该线程对应的本地内存中的共享变量刷新到主内存中;当线程 B 获得锁时,JMM 会把该线程对应的本地内存置为无效。从而使得监视器保护的临界区代码必须从主内存读取共享变量。如下图所示:

其内存语义可理解:

  • 线程 A 释放一个锁,实质上是线程 A 向接下来的获取该锁的其它线程发送一个消息(对共享变量做了修改)
  • 线程 B 获取一个锁,实质上是线程 B 接收了之前某个线程发出的消息(某线程已对共享变量做了修改)
  • 线程 A 释放锁,随后线程 B 获取该锁,这个过程实质上是线程 A 通过主内存向线程 B 发送消息。

锁内存语义的实现依赖于 Java 同步器框架 AbstractQueuedSynchronizer(AQS),后面篇幅再做详细介绍。

五、总结

主要介绍 JMM 内存模型的设计意图,第一为了满足程序员的对代码的易于理解、易于编程;同时内存模型对编译器和处理器的束缚越少越好,这样它们就可能做更多的优化来提高性能。
另外介绍了对 happens-before 的认识及其规则,在具体场景如何体现这种 happens-before 关系来保证程序的正确性。

正文完
 0