JMM的关键技术点都是围绕着多线程的原子性、可见性和有序性来建立的
一、原子性(Atomicity)
原子性是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
比如,对于一个静态全局变量int i,两个线程同时对它赋值,线程A给他赋值1,线程B给它赋值为-1。那么不管这2个线程以何种方式、何种步调工作,i的值要么是1,要么是-1。线程A和线程B之间是没有干扰的。这就是原子性的一个特点,不可被中断。
但如果我们不使用int型而使用long型的话,可能就没有那么幸运了。对于32位系统来说,long型数据的读写不是原子性的(因为long有64位)。也就是说,如果两个线程同时对long进行写入的话(或者读取),对线程之间的结果是有干扰的。
二、可见性(Visibility)
可见性是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。显然,对于串行程序来说,可见性问题是不存在的。因为你在任何一个操作步骤中修改了某个变量,那么在后续的步骤中,读取这个变量的值,一定是修改后的新值。
在CPU1和CPU2上各运行了一个线程,它们共享变量t,由于编译器优化或者硬件优化的缘故,在CPU1上的线程将变量t进行了优化,将其缓存在cache中或者寄存器里。这种情况下,如果在CPU2上的某个线程修改了变量t的实际值,那么CPU1上的线程可能并无法意识到这个改动,依然会读取cache中或者寄存器里的数据。因此,就产生了可见性问题。外在表现为:变量t的值被修改,但是CPU1上的线程依然会读到一个旧值。可见性问题也是并行程序开发中需要重点关注的问题之一。
可见性问题是一个综合性问题。除了上述提到的缓存优化或者硬件优化(有些内存读写可能不会立即触发,而会先进入一个硬件队列等待)会导致可见性问题外,指令重排(这个问题将在下一节中更详细讨论)以及编辑器的优化,都有可能导致一个线程的修改不会立即被其他线程察觉。
三、有序性(Ordering)
有序性问题可能是三个问题中最难理解的了。对于一个线程的执行代码而言,我们总是习惯地认为代码的执行是从先往后,依次执行的。这么理解也不能说完全错误,因为就一个线程内而言,确实会表现成这样。但是,在并发时,程序的执行可能就会出现乱序。给人直观的感觉就是:写在前面的代码,会在后面执行。听起来有些不可思议,是吗?有序性问题的原因是因为程序在执行时,可能会进行指令重排,重排后的指令与原指令的顺序未必一致。
注意:指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致。
哪些指令不能重排:Happen-Before规则
一些基本原则
•程序顺序原则:一个线程内保证语义的串行性
•volatile规则:volatile变量的写,先发生于读,这保证了volatile变量的可见性
•锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
•传递性:A先于B,B先于C,那么A必然先于C
•线程的start()方法先于它的每一个动作
•线程的所有操作先于线程的终结(Thread.join())
•线程的中断(interrupt())先于被中断线程的代码
•对象的构造函数执行、结束先于finalize()方法
volatile与Java内存模型(JMM)
当你用volatile去申明一个变量时,就等于告诉了虚拟机,这个变量极有可能会被某些程序或者线程修改。为了确保这个变量被修改后,应用程序范围内的所有线程都能够“看到”这个改动,虚拟机就必须采用一些特殊的手段,保证这个变量的可见性等特点。比如,根据编译器的优化规则,如果不使用volatile申明变量,那么这个变量被修改后,其他线程可能并不会被通知到,甚至在别的线程中,看到变量的修改顺序都会是反的。但一旦使用volatile,虚拟机就会特别小心地处理这种情况。
volatile可以保证操作的原子性但不能保证复合操作的原子性向i++,数据的可见性和有序性。
在虚拟机的Client模式下,由于JIT并没有做足够的优化,在主线程修改ready变量的状态后,ReaderThread可以发现这个改动,并退出程序。但是在Server模式下,由于系统优化的结果,Read-erThread线程无法“看到”主线程中的修改,导致ReaderThread永远无法退出。
注意:可以使用Java虚拟机参数-server切换到Server模式。和原子性问题一样,我们只要简单地使用volatile来申明ready变量,告诉Java虚拟机,这个变量可能会在不同的线程中修改。这样,就可以顺利解决这个问题了。