Java并发编程那些事儿(九)——内存模型

这是并发编程系列的第九篇文章。上一篇介绍了多线程死锁问题,这一篇说一下Java的内存模型,很多并发问题都是由内存模型决定。

还记得第三篇文章中提到的有关 volatile 的例子吗?

 1public class NoVisibility {
 2    private static volatile  boolean ready = false;
 3    private static int number = 0;
 4
 5    private static class ReaderThread extends Thread {
 6        public void run() {
 7            while (!ready) ;
 8            System.out.println(number);
 9        }
10    }
11
12    public static void main(String[] args) throws InterruptedException {
13        new ReaderThread().start();
14        Thread.sleep(1000);
15        number = 34;
16        ready = true;
17    }
18}

上面的代码, ready 变量如果不使用 volatile 关键 字进行修饰,那么主线程对 ready 的修改,子线程将没办法及时发现。 要想了解这个问题的本质原因,就需要了解 JVM 的内存模型。

多核共享内存模型

现在的计算机几乎都是多核 CPU 了。而每一核都有自己的高速缓存,用于存储一些指令和变量。缓存中的内容,其它核是看不到的,只有同步到主内存之后才能被其他 CPU 看到。如何以及何时将缓存里面的值写入主存凭借的是缓存一致性。大概应该是这个样子。

以上都是硬件相关的背景知识。

重排序

JVM 为了能够提高程序执行性能,会打乱指令的执行顺序,然后将指令分散到多个 CPU 中去执行,以提高性能,只要最终的执行结果与严格的串行环境执行结果一致。

共享变量

为了解决共享变量的正确访问问题, JVM 是这样做的。在 JVM 眼里,变量的读写是个操作,内置锁的加锁和释放是个操作,线程的启动也是个操作。 JVM 为这些操作定义了一个 Happens-Before 的规则。如果想保证执行 A 操作的结果被操作 B 看到,无论 AB 是否在同一个线程,那么 AB 之间需要满足 Happens-Before 的关系。如果两个任意的操作之间没有 Happens-Before 关系,那么 JVM 就会对指令进行重排序。

Happens-Before规则,引自《Java并发编程实战》。

程序次序规则 :一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;

锁定规则 :一个unLock操作先行发生于后面对同一个锁额lock操作;

volatile变量规则 :对一个变量的写操作先行发生于后面对这个变量的读操作;

传递规则 :如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;

线程启动规则 :Thread对象的start()方法先行发生于此线程的每个一个动作;

线程中断规则 :对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;

线程终结规则 :线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;

对象终结规则 :一个对象的初始化完成先行发生于他的finalize()方法的开始;

结束

简单说如果两个指令之间没有 Happens-Before 规则,那么 JVM 就有可能对指令进行重排序,如果有重排序发生,那么我们在多线程的程序中就有可能看到共享变量的不一致状态。