Java多线程(三)——内存模型

Java内存模型,Java Memory Model,JMM,是一个抽象概念,是Java内存使用的一组规则和规范。主要围绕原子性、有序性、可见性展开。在JMM中,内存不划分为两大块,主内存和工作内存。主内存存储对象实例,其实就相当于JVM中的Heap,而工作内存存储当前方法的变量信息,可以理解为栈。

主内存是线程共享的,而工作内存是各个线程独享的。就算两个线程运行的是同一段代码,它们也是在不同的栈中。相反,如果两个线程操作同一个主空间(heap)里对象实例,那么他们会先把主内存中的对象拷贝到栈中,操作执行完成后再刷新到主内存中。显而易见,这里会发现线程安全问题,而JMM就是解决这个问题的规则。
首先,我们来看看JMM的三个特性

1.原子性

原子性意味着不可拆分,独立,不被其他东西影响。也就是说,原子性操作在多线程环境下是不可中断的。
计算机在执行程序时,为了提高性能,编辑器和处理器会对指令重新排序。
编译器优化的重排:编译器在不改变单线程程序语义的情况下,可以重新安排语句的顺序。
指令并行重排:现代处理器使用了指令级并行技术将多条指令重叠执行,如果不存在结果依赖性,处理器可以改变语句对应的机器指令的执行顺序。
内存系统的重排:由于处理器使用缓存和读写缓冲区,这使得加载和存储看起来像是在乱序执行。因为三级缓存的存在,内存和缓存的数据同步存在着时间差。

2.可见性

可见性是指,一个线程修改了共享变量的值,其他线程是否能够马上拿到修改后的值。对于串行程序来说,可见性没有意义,因为它是顺序执行的。之前提到过,多线程在操作同一个对象实例的时候,是先把Heap中的对象拷贝到栈中去,操作完毕后再把新值写回到Heap。假如线程A从Heap中拿到对象X,并且修改值,那么很有可能在它把X写回到Heap前,X又被线程B拷贝到自己的栈中,进行操作。那么此时对于线程B来说,线程A对于X的操作,线程B是不知道的。

3.有序性

对于单线程的代码来说,它的执行是我们可以认为是从上向下顺序执行的。但是因为之前提到的重排问题,实际上代码的执行顺序并不一定和我们写的一致。

happens-before

除了sync和volatile之外,JMM还定义了happens-before来保证程序执行的原子性、可见性和有序性。它有好几个组成部分。
程序顺序原则,在同一个线程内,保证语义的串行性,可以单纯的理解为代码顺序执行。
锁规则,unlock必然发生在同一个锁的lock之后,如果再次加锁,那么lock必然发生在unlock之后。
volatile规则,volatile变量的写县发生于读,可以简单的理解为,当volatile发生读操作的时候,都强制从Heap中读取,同样的,发生写操作时,都强制要求立刻回写到Heap中。这样所有的线程看到的值都是最新的。
传递性,A先于B,B先于C,那么A先于C。
线程启动规则,线程的start()方法,优先于它的每一个动作,线程A在执行线程B的start()的方法前,如果修改了共享变量的值,那么线程Bstart后,可以拿到线程A执行的后的最新值。
线程终止规则,线程的所有操作都优先于线程的终止。Thread.join()方法的作用是等待当前执行线程终止,如果线程b中调用了线程a的join方法,那么线程b会等到线程a执行完毕后再继续执行。如果线程a执行过程中修改了共享变量,那么线程b启动后可以拿到最新的值。
线程中断原则,对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。
对象终结原则,对象的构造函数执行先于finalize方法。

volatile是JVM提供的轻量级同步机制,保证volatile修饰的共享变量对所有线程可见,并且禁止指令重排优化。