要想在日常开发中写出高性能高可靠的Java服务,熟练掌握并发编必不可少的。要想熟练掌握并发编程的技巧,就得先厘清一些和并发相关的基础理论知识,而JMM就是Java并发的核心基础。下面是是本文将描述的几个核心问题:
- JMM是什么?
- 重排序
- happens-before
- 内存屏障
- Java相关
- 总结
JMM是什么?
Java内存模型[以下称JMM]是什么?先看看什么是内存模型.现在的多核处理器系统中1,处理器通过多级缓存来加快数据访问,提高了性能的同时也面临了一个挑战:当两个及以上处理器同时查看同一个内存位置(比如更新同一个变量)的时候会怎么样,能看到相同的值吗?这个问题对初学者来说很抽象,观察同一个内存位置难道看到的值会不一样吗?答案是可能不一样,因为程序在运行的时候所读取的数据是来自缓存(先看一级缓存有没有,没有再去二级缓存拿,如果所有缓存都没有就会去主存加载),可见程序使用的变量值是内存的一份拷贝,如果在程序计算的过程中主存中的值发成变化了,那么当前处理器就无法感知了。
这个时候内存模型就派上用场了。内存模型定义了一个处理器如何才能知道其他处理器对内存(变量)的更新。(请记住这一点) 内存模型可分为:
- 强内存模型(strong memory model):所有核心在任何时候看到的值总是相同的。
- 弱内存模型(weak memory model):不同的核心可能会看到不同的值,有特殊的缓存规则控制这种情况出现的时机。
强内存模型看上去对程序员来说更加友好,但是其效率低下。JMM则属于更符合真实 CPU 架构的发展趋势的弱内存模型。
重排序
在描述JMM的一写重要定义前,先看看一个多线程需要面临的一个问题——重排序[Reordering]。简单的说就是程序的执行顺序可能与程序指定的顺序不一致。为了提高执行效率,javac、JIT和CPU指令执行都可能改变代码的执行。重排序保证的是不改变单线程的执行结果。
CPU避免内存访问延迟最常见的技术是将指令管道化,然后尽量重排这些管道的执行以最大化利用缓存,从而把因为缓存未命中引起的延迟降到最小。编译器和CPU可以自由的重排指令以最佳的利用CPU,只要下一次循环前更新该计数器即可。
happens-before
再回到JMM,JMM的定义是通过动作(actions)的形式进行描述的,所谓动作,包括变量的读和写、管程(monitor)的加锁和释放,线程的启动和拼接(join)2。如上图,我们通过Java 并发编程实践中的例子来描述怎样才能在线程B中看到线程A的执行结果——也就是happens-before法则。happens-before就是发生在某某动作之前的意思。happens-before法则包括:
- 程序次序法则:线程中的每个动作 happens-before 该线程中后续动作,该线程中的每个动作都按程序顺序进行。
- 管程锁[monitor]法则:对一个管程的解锁 happens-before 每一个后续对同一个管程的加锁。(Sychronized语义)
- volatile变量法则:对volatile字段的写入操作 happens-before 每一个后续的读操作。
- 线程start法则:Thread.start 调用 happens-before 该线程的后续所有动作。
- 线程终止法则:线程中的任何动作都 happens-before 线程的终止。
- 线程join法则:线程中的所有操作 happens-before 任何其他线程从该线程上的join()成功返回。
- 线程interrupt法则:一个线程调用另一个线程的 interrupt happens-before 中断线程发现中断。
- finalizer法则:一个对象的构造函数的结束 happens-before 这个对象的finalizer的开始。
- 传递性:如果 A happens-before B,B happens-before C,那么 A happens-before C。、
由类库担保的 happens-before 排序:
- 将一个 item 放入thread-safe容器 happens-before 另一个线程从该容器中获取 item。
- 锁存器(CountDownLatch)的countDown() happens-before 线程从锁存器的await() 中返回。
- 信号量(Semaphpre)的release() 一个 permit happens-before 同一个信号量的aquire()。
- Future中代表的任务多发生的动作 happens-before 另一个线成功的从Future.get返回。
- Excutor的submit happens-before 任务的执行。
- 一个线程到达CyclicBarrier或者Exchanger happens-before 其他线程的释放。
内存屏障
什么是内存屏障?3内存屏障是一个CPU指令。前面说过,编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。内存屏障另一个作用是强制刷新CPU的缓存,这样能实现多线程的可见性。内存屏障指令正是JMM happens-before法则的底层实现。
内存屏障指令分类
- LoadLoad:禁止读和读的重排序。
- StoreStore:禁止写和写的重排序。
- LoadStore:禁止读和写的重排序。
- StoreLoad:禁止写和读的重排序。
内存屏障的更多细节可参考这里。
JDK中的内存屏蔽
public final class Unsafe {
...
// loadFence=LoadLoad+LoadStore
public native void loadFence();
// storeFence=StoreStore+LoadStore
public native void storeFence();
// fullFence=loadFence+storeFence+StoreLoad
public native void fullFence();
...
}
Java相关
volatile
如果你的字段是volatile,Java内存模型将在写操作后插入一个写屏障指令,在读操作前插入一个读屏障指令。volatile的可见性使其在观察状态变化的应用中得到广泛使用。经过volatile修饰的字段,在编译之后会有一个ACC_VOLATILE
标识。volatile的语义4:
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。
- 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
...
public volatile int i;
descriptor: I
flags: (0x0041) ACC_PUBLIC, ACC_VOLATILE
...
volatile实现原理
- 在volatile写操作的前面插入一个StoreStore屏障。保证volatile写操作不会和之前的写操作重排序。
- 在volatile写操作的后面插入一个StoreLoad屏障。保证volatile写操作不会和之后的读操作重排序。
- 在volatile读操作的后面插入一个LoadLoad屏障+LoadStore屏障。保证volatile读操作不会和之后的读操作、写操作重排序。
总结
JMM是Java并发编程的理论基础,对开发安全的并发程序至关重要。如果需要了解更详细的JMM知识,看这里。