线程通信与同步在并发编程中,有两个需要处理的关键问题:
通信指线程之间以何种机制来交换信息,通信机制有两种:
同步是指程序中用于控制不同线程间操作发生的相对顺序的机制。在共享内存并发模型中,同步是显式进行的,程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式的 Java 内存模型前面提到线程的通信与同步问题,Java 线程之间的通信由 Java 内存模型(简称 JMM)控制,JMM 决定一个线程对共享变量的写入何时对另一个线程可见 线程之间的共享变量存储在主内存,每个线程都一个私有的本地内存,本地内存中存储了该线程以 读/写 共享变量的副本 Java 内存模型的抽象示意如图 如果线程 A 和线程 B 之间要通信的话,必须经过下面两个步骤:
这两个步骤实际上是线程 A 向线程 B 发送消息,而且这个通信过程必须经过主内存。JMM 通过控制主内存与每个线程的本地内存之间的交互,来为 Java 程序员提供内存可见性保证 重排序在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序可能会导致线程程序出现内存可见性问题,下面分别介绍三种类型的重排序以及它们对内存可见性的影响:
所以 Java 源代码到最终实际执行的指令序列,会分别经历以下三种重排序 JMM 保证内存可见性由此可见,JMM 不能任由重排序发生,必须加以控制,否则会引发线程不安全问题。为了更好地解释 JMM 为保证内存可见性所采取的措施,首先介绍一些基础概念 1. 数据依赖性如果两个操作访问同一个变量,且这两个操作有一个为写操作,此时这两个操作之间就存在数据依赖性,只要重排序这两个操作的执行顺序,程序的执行结果就会被改变。编译器和处理器在重排序时,会遵守数据依赖性,不会改变存在数据依赖关系的两个操作的执行顺序。数据依赖分为下列三种类型:
上面三种情况,只要重排序两个操作的执行顺序,程序的执行结果就会被改变 这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑 2. as-if-serial 语义as-if-serial 语义的意思是:不管怎么重排序,(单线程)程序的执行结果不能被改变。编译器、runtime 和处理器都必须遵守 as-if-serial 语义,为了遵守 as-if-serial 语义,编译器和处理器不会对存在数据依赖性关系的操作做重排序 as-if-serial 语义把单线程程序保护起来,给程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的,程序员无需担心重排序会干扰他们,也无需担心内存可见性问题 3. happens-before 原则happens-before 是 JMM 最核心的概念,对于 Java 程序员来说,理解 happens-before 是理解 JMM 的关键。 从 JDK5 开始,Java 使用新的 JSR-133 内存模型,JSR-133 使用 happens-before 的概念来阐述操作之间的内存可见性。在 JMM 中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在 happens-before 关系,这两个操作既可以是在一个线程之内,也可以是不同线程之间 A happens-before B,就是 A 操作先于 B 操作执行。当然这种说法并不准确,两个操作之间具有 happens-before 关系,仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前 在 JMM 中定义了 happens-before 的原则如下:
有关 happens-before 每一个原则的实现,这里不再具体阐述,只要知道有这么一回事就好了 4. 顺序一致性内存模型顺序一致性内存模型是一个被计算机科学家理想化了的理论参考模型,它为程序员提供了极强的内存可见性保证。顺序一致性内存模型有两大特征:
顺序一致性内存模型为程序员提供的视图如下: 在概念上,顺序一致性模型有一个单一的全局内存,这个内存通过一个左右摆动的开关可以连接到任意一个线程。同时,每一个线程必须按程序的顺序来执行内存读/写操作。从上图我们可以看出,在任意时间点最多只能有一个线程可以连接到内存。当多个线程并发执行时,图中的开关装置能把所有线程的所有内存 读/写 操作串行化 为了更好的理解,下面我们通过两个示意图来对顺序一致性模型的特性做进一步的说明 假设有两个线程 A 和 B 并发执行,其中 A 线程有三个操作,它们在程序中的顺序是:A1 -> A2 -> A3,B 线程也有三个操作,它们在程序中的顺序是:B1 -> B2 -> B3 假设这两个线程使用监视器来正确同步:A 线程的三个操作执行后释放监视器,随后 B 线程获取同一个监视器。那么程序在顺序一致性模型中的执行效果将如下图所示: 现在再假设这两个线程没有做同步,下面是这个未同步程序在顺序一致性模型中的执行示意图: 未同步程序在顺序一致性模型中虽然整体执行顺序是无序的,但所有线程都只能看到一个一致的整体执行顺序。以上图为例,线程 A 和 B 看到的执行顺序都是:B1->A1->A2->B2->A3->B3。之所以能得到这个保证是因为顺序一致性内存模型中的每个操作必须立即对任意线程可见 5. 总结由于重排序的存在,JMM 不可能实现顺序一致性内存模型,同时也不可能完全禁止重排序,因为这样会影响效率。一方面,程序员希望内存模型易于理解、易于编程,希望基于一个强内存模型来编写代码;另一方面,编译器和处理器希望内存模型对它们的束缚越少越好,这样它们就可以做尽可能多的优化来提高性能,编译器和处理器希望实现一个弱内存模型。这两个因素相互矛盾,所以关键在于找到一个平衡点 平衡的关键在于优化重排序规则,根据前面提到的 happens-before 原则、数据依赖性以及 as-if-serial 原则等规定了编译器和处理器什么情况允许重排序,什么情况不允许重排序。对于会改变程序执行结果的重排序,JMM 要求编译器和处理器必须禁止这种重排序,否则不作要求。于是程序员所看到的就是一个保证了内存可见性的可靠的内存模型 下图是 JMM 的设计示意图 从上图我们也可以发现,JMM 会遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都可以。例如,如果编译器经过细致地分析后,认定一个锁只会被单个线程访问,那么这个锁可以被消除。再如,如果编译器经过细致的分析后,认定一个 volatile 变量只会被单个线程访问,那么编译器可以把这个 volatile 变量当作一个普通变量来对待。这些优化既不会改变程序的执行结果,又能提高程序的执行效率。而从程序员的角度来看,程序员其实并不关心重排序是否真的发生,程序员关心的是只程序执行时的语义不能被改变而已 |
|