相信如果对JMM底层有过了解或者接触过java并发编程的读者对以上的概念并不陌生,但是真正理解的可能并不多。这里我就对这些概念再做一次讲解。相信读者多读几遍应该就有自己的理解,实在不理解也没关系,说明知识的储备还不够,不妨以后再来读一遍,可能会瞬间突然明白。
参考内容:
- https://mp.weixin.qq.com/s/kQ498ifh4OUEDd829JIhnQ 作者:CodeSheep;他同时也是B站up主,ID就是CodeSheep,视频良心,有兴趣的读者可以自己查看。
- 《实战Java高并发程序设计》 作者:葛一鸣 郭超 ;如果是并发编程初学者我也推荐这本书,通俗易懂
JMM的关键技术点都是围绕着多线程的原子性、可见性和有序性来建立的。要理解JMM首先就需要了解这三个特性。而volatile关键字很好的贯彻了可见性和有序性,提到volatile关键字也是为了加深对这三个特性的理解,同时volatile也是非常重要的内容,也是难点。
1、原子性
原子性其实非常好理解,原子性操作就是指这些操作是不可中断的,要做一定做完,要么就没有执行,也就是不可被中断。
我们使用的int类型的数据如果只是是简单的读取和赋值的话就是原子操作。下面给出几个例子:
i = 2; 赋值给i -----------------------------操作步骤:1 原子操作
j = i; 读取i值 赋值给j -----------------------操作步骤:2 非原子操作
i++; 读取i值 i值加1 赋值给i ------------------操作步骤:3 非原子操作
i = i+1; 读取i值 i值加1 赋值给i --------------操作步骤:3 非原子操作
但是如果我们不使用int型而使用long型的话,对于32位系统来说,long型数据的读写不是原子性的(因为long有64位)。虚拟机规范中允许对 64位数据类型( long和 double),分为 2次 32为的操作来处理,也就是说,如果两个线程同时对long进行写入的话(或者读取),对线程之间的结果是有干扰的。可能高位是一个线程写的,低位又是另一个线程写的,如果这时候读的话,就会读到错误的值。不是线程1写的值,也不是线程2写的值。但是最新 JDK实现还是实现了原子操作的。JMM只实现了基本的原子性,像上面 i++那样的操作,必须借助于 synchronized和 Lock来保证整块代码的原子性了。
2、可见性
可见性是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。
显然,对于串行程序来说,可见性问题是不存在的。因为你在任何一个操作步骤中修改了某个变量,那么在后续的步骤中,读取这个变量的值,一定是修改后的新值。但是这个问题在并行程序中就不见得了。如果一个线程修改了某一个全局变量,那么其他线程未必可以马上知道这个改动。这个问题可能由cache优化引起,比如下面这个例子:
如果在CPU1和CPU2上各运行了一个线程,它们共享变量t,由于编译器优化或者硬件优化的缘故,在CPU1上的线程将变量t进行了优化,将其缓存在cache中或者寄存器里。这种情况下,如果在CPU2上的某个线程修改了变量t的实际值,那么CPU1上的线程可能并无法意识到这个改动,依然会读取cache中或者寄存器里的数据。因此,就产生了可见性问题。外在表现为:变量t的值被修改,但是CPU1上的线程依然会读到一个旧值。可见性问题也是并行程序开发中需要重点关注的问题之一。
可见性问题是一个综合性问题。除了上述提到的缓存优化或者硬件优化(有些内存读写可能不会立即触发,而会先进入一个硬件队列等待)会导致可见性问题外,指令重排(这个问题将在下一节中更详细讨论)以及编辑器的优化,都有可能导致一个线程的修改不会立即被其他线程察觉。
3、有序性
JMM是允许编译器和处理器对指令重排序的,但是规定了 as-if-serial语义,即不管怎么重排序,程序的执行结果不能改变。比如下面的程序段:
double pi = 3.14; // A
double r = 1; // B
doubles = pi *r *r; // C
无论是 A->B->C 还是 B->A->C 都对结果没有影响。但是这是发生在单线程之中的。在多线程之中可能就不是这样了,多线程有序性引起的问题我们可以看一个典型的例子:
class OrderExample{