Java并发拾遗(一)——并发、JMM、重排序与可见性
一、并发中的关键问题及其解决思路
并发中的关键问题:
1. 线程之间如何通信 —— 线程之间如何交换信息
2. 线程之间如何同步 —— 控制线程的相对执行顺序
两种解决思路:
1. 隐式通信,显示同步 —— 线程之间通过共享内存中的公共状态来隐式通信,那么就必须显示的指定线程见的互斥来实现同步
2. 显式通信,隐式同步 —— 线程之间无公共状态,通过明确的发送消息进行通信,那么由于消息的发送在消息接收之前,就可以实现隐式的同步
Java选择了共享内存的方式来解决并发中的两个关键问题,因此Java中线程的通信是隐式的,对程序员透明,但需要程序员自己来控制线程的同步,如使用互斥锁等。
二、JMM
JMM(Java memory model),控制Java线程之间的通信,定义了线程私有的本地内存和共享的主内存之间的关系,决定了一个线程对共享变量的写入何时对另一个线程可见。
如上图,Java不同线程的通信必须经过主内存,JMM通过控制主内存与每个线程的本地内存的交互,来向程序员提供了内存可见性。
三、重排序
在编译或执行时,编译器或处理器常常会对指令进行重排序,有以下三种情况:
1. 编译器重排序:Java编译器通过对Java代码语义的理解,根据优化规则对代码指令进行重排序。
2. 机器指令级别的重排序:现代处理器很聪明,能够自主判断和变更机器指令的执行顺序。
3. 内存系统的重排序:由于处理器与主内存之间存在缓冲,导致在加载和存储操作可能在乱序执行。
其中,1属于由Java来指定规则的编译器重排序,2和3属于处理器级别的重排序。这些重排序就可能导致并发程序出现一些奇怪的问题,比如——可见性问题。
既然重排序导致了问题,那就在一些关键点,禁止重排序,就可以避免这些问题了。首先是编译期的禁止重排序,由于编译期的重排序规则就是由Java编译期来制定的,那么Java肯定能做到这种禁止。而在运行期的处理器级别的禁止重排序,就需要通过插入特定类型的内存屏障指令(memory barries / memory fence)来禁止重排序(可想而知,这必然需要操作系统与处理器级别提供的支持)。JMM就是通过这两个层面的禁止重排序保证了一致的内存可见性。
四、可见性
即线程A开始执行写操作之后,B啥时候能看到。由JMM可知,A执行了写操作之后,把结果缓存在自己的本地内存中是不行的,B是看不到的,只有当A将缓存刷回主内存之后,B才能看见。在JMM中,使用happens-before来描述两个操作之间的关系,当A操作的执行结果需要对操作B可见时,就说要求 A happens-before B。
JMM模型即提供了一些happens-before规则,这些规则由JDK来保证,使得程序员可以放心的新任和依赖:
1. 程序顺序:代码按书写规则,前面的代码 happens-before 后面的代码
2. 对一个volatile变量的写 happens-before 后续对这个变量的读
等等,正是这些由Java自身保证的happens-before规则,才让程序员们可以放心的使用Java编写并发程序。