为了规避多线程的问题,Java制定了一套内存模型与线程规范。Java内存模型描述了线程之间是如何通过内存进行交互的。开发人员可以按照这个规范开发出符合线程安全的程序,JVM需要严格按照这个规范来执行Java程序。这个规范是对开发者的一个使用承诺,也是对JVM的一个硬性要求。JSR-133: Java Memory Model and Thread Specification中详细描述了多线程下的内存规范,定义了线程间共享内存的可见性。
JVM的运行时内存布局,总体上可以分为堆内存与线程栈内存(参见2.3节)。堆内存存储全局对象数据以实现多线程共享,栈内存存储线程执行的相关信息,栈内存无法共享,如图3-8所示。
图3-8 JVM内存布局
所有对象实例、static字段、数组元素都存储在堆内存中,能够实现线程共享。基本数据类型的本地变量都完全存储在线程堆栈中,无法实现线程共享。方法的输入参数以及异常信息无法在多线程之间共享。一个线程可以将基本数据类型的变量作为入口参数传递给另一个线程,但是它不能共享局部变量本身。异常信息属于线程私有信息,无法跨线程传递。
JMM对共享变量的可见性做了如下描述。当多个线程共享同一个变量,一个线程对普通变量做出修改,其他线程也可以感知到,但不是实时感知的,同步时间依赖于程序的调度与CPU缓存自身的同步机制。被volatile关键字修饰的变量在修改时能够实现线程间的实时可见。对象加锁和解锁操作是实时可见的,一个线程对一个对象加锁,另外一个线程能够实时感知。一个线程的启动与停止对另一个线程是实时可见的,一个线程能实时感知到另一个线程启动与结束的状态。
从中文字面意思上来说,Happens-Before很容易理解成一个操作发生在后续操作的前面,这是文化差异造成的误解。实际上,Happens-Before是指多线程共享一个变量时,前面线程对变量的修改对后面的线程可见。Happens-Before规则就是要保证线程之间的共享变量可见性。JVM的编译器在对代码进行编译时需要遵循Happens-Before原则,确保编译器优化后程序的执行结果也遵守Happens-Before原则。
1.程序的顺序性规则
这条规则是指在一个线程内部,线程执行的顺序需要按照程序语义顺序,前面操作产生的结果必须对后面操作可见。例如对一个变量V的操作,写操作要出现在读操作后面。代码清单3-5是一个程序顺序执行的示例。
代码清单3-5 程序顺序性执行的示例
编译器在执行时可以对第2~4行的代码的执行顺序随便优化,但是不能调整第5行result=x+y;的顺序,如果调整了,执行结果就不对了。
2.volatile变量规则
这条规则用于对volatile关键字行为进行约束。一个volatile变量的写操作对后续该变量的读操作遵循Happens-Before原则。即一个线程对volatile变量的修改对另外一个线程实时可见。
3.传递原则
这条规则是简单的逻辑推导原则。如果A操作的结果对B操作可见,且B操作的结果对C操作可见,那么A操作的结果一定对C可见。这个原则光从理论上难以直观解释,代码清单3-6是简单的传递原则的示例。
代码清单3-6 传递原则的示例
LogicRule中定义了size与start两个变量,其中start变量是用volatile修饰的,具备实时可见性。同时启动的两个线程:线程A与线程B,线程A开始对size的值进行修改,然后对start值进行修改。线程B则是先读取start的值,然后读取size的值。线程值传递的逻辑如图3-9所示。
图3-9 线程值传递
在线程A中size=100与写变量start=true遵循Happens-Before原则,同时start变量是用volatile修饰的,所以线程B可以看到start与size的修改。
4.锁的规则
这条规则是指锁状态的变更在线程间是实时可见的,一个线程释放了锁,等待获取锁的线程能够实时获取这个锁。在Java中,这个锁大多数时候通过synchronized来实现。如果有多个线程通过synchronized来实现线程同步,永远只会有一个线程可以拿到锁对象。当其中一个线程执行数据修改完成后会释放锁对象,另一个线程会拿到锁对象,拿到锁对象的线程就可以看到前面线程对数据的修改结果了。代码清单3-7是一个用synchronized实现线程加锁同步的示例。
代码清单3-7 锁规则示例
LockRule类里面定义了start变量,然后定义了A和B两个线程。线程A先执行、线程B后执行。A与B两个线程都会通过synchronized来获取同一个锁对象,线程A执行完start=100才会释放锁对象。当线程B获取到锁对象后,会看到线程A对start的修改,打印出start=100的结果。
5.start规则
这个规则是用来确保在调用start方法启动线程之前,父线程已经对变量做出的修改在子线程中是实时可见的。代码清单3-8是一个start规则的示例。
代码清单3-8 start规则示例
StartRule定义了start变量,然后定义了线程来打印start变量。第11行将start赋值为100,在代码执行的过程中,我们可以清晰地看到结果是:start=100。
6.join规则
这条原则是针对线程间同步等待的,当一个线程A等待另一个线程B运行结束,当线程B运行结束后,线程A要能看到线程B对共享变量的修改。例如,主线程通过join方法来等待子线程结束,当子线程执行结束后,主线程能够看到子线程对共享变量的修改。代码清单3-9是join规则的示例。
代码清单3-9 join规则示例
JoinRule定义了start变量,然后定义了线程来修改start的值。主线程通过join方法来同步等待子线程执行结束,然后打印start变量。我们可以清晰地看到start的值为100。
7.final字段的特殊性
final修饰的字段无论在单线程还是多线程里都是线程安全的不可变对象。当一个对象的构造函数执行完成时,可以认为该构造函数进行了完全的初始化。只有在对象完全初始化后才能被对象的引用线程看到。在构造函数为final字段赋值和在另一个线程中读取对象的值这两个动作之间,有一个Happens-Before的机制。在构造函数执行完成之前,所有对于final字段的写操作都会被冻结,只有构造函数完成之后才能进行字段读取。