Java内存模型可以说是Java并发的底层支持,了解Java内存模型才能正在了解Java并发。
内存模型
在内存中设置一个变量"value = 1;"那么其他线程能在什么时候读取到这个结果呢?有可能不能立即甚至永远都读不到。比如指令顺序与源代码中的顺序不同;编译器会把变量保存在寄存器而不是内存中;处理器可以采用乱序或并行等方式来执行指令;缓存可能会改变将写入变量提交到主内存的次序,保存在处理器本地缓存中的值其他处理器是不可见的;这些情况都会导致其他线程不能读取到变量的最新值。
而Java内存模型(Java memory Model,简称JMM)则是规定了JVM在什么时候对变量的修改对其他线程可见。
在多核处理器中,每个处理器都有自己的缓存,并且定期地与主内存进行协调,不同的处理器架构中提供了不同级别的缓存一致性。在现在的CPU中分了多级内存缓存比如寄存器、L1、L2、L3、内存等,每种处理器都有各自的规则和处理方式,而要保证修改变量对其他线程可见的难度就很大。
所以在JMM中抽象出来只分工作内存、主内存。主内存主要存共享变量,工作内存为每个线程拥有,存放线程需要的共享变量副本。各个线程只能读、改自己工作线程的数据,不能直接操作主内存的变量,线程修改变量时先修改工作内存变量再同步到主内存当中。
在多线程环境中,维护程序的串行性将会导致很大的性能开销,所以只有当多个线程要共享数据时,才必须协调它们之间的操作,并且JVM依赖程序通过同步操作来找出这些协调操作在何时发生。通过只分两种内存就简单很多了。
Happens-Before规则
上一节说到JVM通过依赖同步操作来找出协调操作在何时发生,而JMM就是通过各种操作来定义的。JMM对程序中的所有操作定义了一个偏序关系,称为Happens-Before。
Happens-before简单解释下:如果第一个操作Happens-before第二个操作,也就是说第一个操作对于第二个操作时可见的,也就是第二个操作能够看到第一个操作的结果。
而Happens-before主要包含以下规则:
程序顺序规则:一个线程内肯定要保证执行顺序,比如两步代码前一步执行肯定要在下一步执行之前,如果不能保证规则那么如果后一步依赖前一步的结果那么肯定会出现错误。不过这个规则和指令重排冲突,但是执行重排是在保证执行结果依然符合Happens-before执行的结果下才重排,所以并不冲突。
监视器规则:对同一个锁,肯定前面一个释放了锁,后面一个才能获取到锁,只有获取到锁才能释放锁。
volatile变量规则:volatile修饰的变量在一个线程修改后,其他线程一定能够看到最新值。
线程启动规则:在主线程执行一个子线程,那么子线程的run方法一定能够看到主方法调用子线程的start方法之前的操作。
线程结束规则:主线程调用了子线程的start后如果再调用join方法,那么join方法肯定能看到子线程run方法执行的结果。
中断规则:对线程执行Interrupt方法后,那么执行interrupted和isInterrupted都能看到结果。
终结器规则:对象的构造函数必须在对象的终结器执行前完成。
传递性:A操作在B之前,B在C之前,那么A一定在C之前,也就是C一定能够看到A执行的结果。
在多线程中每个线程每段代码执行的时间是不确定的,而Happens-before则保证了单个线程内执行顺序,同时也保证在多线程哪些情况下有先后顺序。比如比如在线程A中执行了线程B的start方法和B线程的join方法,那么B线程的run方法肯定在A在调用B的start方法之后执行,也就是B中的run方法能看到之前的执行结果。同样join方法一定是run方法结束以后才能执行,也就是join之后的程序能够看到run执行的结果。
单例模式的双重检查
单例模式的一种实现方式代码如下图:
volatile保证了变量的可见性,就是前面讲到的volatile变量规则,在第二次验证变量singleton时才能得到的正确。如果变量没有用volatile修饰一个线程初始化了,初始化结果可能还在工作内存中,即使同步到主内存中,但是如果没有同步到其他内存中,那么其他线程就可能再次初始化。
总结
JMM实际上是由定义的一系列操作组成,这些操作确定了Java的基础特性,尤其在多线程并发方面,它主要对重排序、原子性、内存可见性这三个方面维护保证了多线程的正确执行。
Java程序员日常学习笔记,如理解有误欢迎各位交流讨论!