并发编程之happens-before

前言
Jdk5开始,Java使用新的JSP-133内存模型,JSR-133使用happens-before的概念来阐述操作直接的内存可见性,那么这两个操作之间必须要存在happen-before关系。

一、文章导图

导图.png

二、Happens-Before

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

1、JMM的设计意图

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

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

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

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

JMM设计示意图.jpeg

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()操作成功返回。

如下示例说明:
happens-before示例.jpeg
如图:按程序顺序规则知,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会把该线程对应的本地内存置为无效。从而使得监视器保护的临界区代码必须从主内存读取共享变量。如下图所示:
锁获取示意图.png
其内存语义可理解:

  • 线程A 释放一个锁,实质上是线程A向接下来的获取该锁的其它线程发送一个消息(对共享变量做了修改)
  • 线程B 获取一个锁,实质上是线程B接收了之前某个线程发出的消息(已对共享变量做了修改)
  • 线程A释放锁,随后线程B获取该锁,这个过程实质上是线程A通过主内存向线程B发送消息。
锁内存语义的实现依赖于Java同步器框架 AbstractQueuedSynchronizer(AQS),其它篇幅再做详细介绍。

五、总结

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章