LoginSignup
1
1

More than 1 year has passed since last update.

Java Memery Model

Posted at

要想在日常开发中写出高性能高可靠的Java服务,熟练掌握并发编必不可少的。要想熟练掌握并发编程的技巧,就得先厘清一些和并发相关的基础理论知识,而JMM就是Java并发的核心基础。下面是是本文将描述的几个核心问题:

  • JMM是什么?
  • 重排序
  • happens-before
  • 内存屏障
  • Java相关
  • 总结

JMM是什么?

Java内存模型[以下称JMM]是什么?先看看什么是内存模型.现在的多核处理器系统中1,处理器通过多级缓存来加快数据访问,提高了性能的同时也面临了一个挑战:当两个及以上处理器同时查看同一个内存位置(比如更新同一个变量)的时候会怎么样,能看到相同的值吗?这个问题对初学者来说很抽象,观察同一个内存位置难道看到的值会不一样吗?答案是可能不一样,因为程序在运行的时候所读取的数据是来自缓存(先看一级缓存有没有,没有再去二级缓存拿,如果所有缓存都没有就会去主存加载),可见程序使用的变量值是内存的一份拷贝,如果在程序计算的过程中主存中的值发成变化了,那么当前处理器就无法感知了。

这个时候内存模型就派上用场了。内存模型定义了一个处理器如何才能知道其他处理器对内存(变量)的更新。(请记住这一点) 内存模型可分为:

  • 强内存模型(strong memory model):所有核心在任何时候看到的值总是相同的。
  • 弱内存模型(weak memory model):不同的核心可能会看到不同的值,有特殊的缓存规则控制这种情况出现的时机。

强内存模型看上去对程序员来说更加友好,但是其效率低下。JMM则属于更符合真实 CPU 架构的发展趋势的弱内存模型。

16640300606093.jpg

重排序

在描述JMM的一写重要定义前,先看看一个多线程需要面临的一个问题——重排序[Reordering]。简单的说就是程序的执行顺序可能与程序指定的顺序不一致。为了提高执行效率,javac、JIT和CPU指令执行都可能改变代码的执行。重排序保证的是不改变单线程的执行结果。

CPU避免内存访问延迟最常见的技术是将指令管道化,然后尽量重排这些管道的执行以最大化利用缓存,从而把因为缓存未命中引起的延迟降到最小。编译器和CPU可以自由的重排指令以最佳的利用CPU,只要下一次循环前更新该计数器即可。

happens-before

happens-before.png

再回到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法则的底层实现。
16640693387348.png

内存屏障指令分类

  1. LoadLoad:禁止读和读的重排序。
  2. StoreStore:禁止写和写的重排序。
  3. LoadStore:禁止读和写的重排序。
  4. 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实现原理

  1. 在volatile写操作的前面插入一个StoreStore屏障。保证volatile写操作不会和之前的写操作重排序。
  2. 在volatile写操作的后面插入一个StoreLoad屏障。保证volatile写操作不会和之后的读操作重排序。
  3. 在volatile读操作的后面插入一个LoadLoad屏障+LoadStore屏障。保证volatile读操作不会和之后的读操作、写操作重排序。

16693572686998.png

总结

JMM是Java并发编程的理论基础,对开发安全的并发程序至关重要。如果需要了解更详细的JMM知识,看这里。

  1. Java性能优化实践:JVM调优策略、工具与技巧

  2. Java并发编程实践

  3. memory-barrier

  4. volatile 参考

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1