现代编程离不开并发,但并发效率提升的同时也带来了一些问题,比如程序奇怪的执行顺序,诡异的行为,Java 为了解决这个问题提出了一套内存模型 (JMM),同时我们需要注意的是,我们真正要了解的是什么,是 Java语言规范关于 JMM 定义的规则,还是 JVM 对于 JMM 的的具体实现,本文主要根据
JSR 133
讨论 Java 语言规范 关于 JMM 定义的规则
顺序一致模型
顺序一致性是程序执行过程中可见性和顺序的强有力保证。在顺序一致的执行过程 中,所有动作(如读和写)间存在一个全序关系,与程序的顺序一致。
通俗的讲保证程序执行的顺序与代码的顺序一致(不会由于编译器的优化而发生重排序),但其并不意味着并发安全。
每个动作都是原子的且立即对所有线程可见。如果一个程序没有数据争用,那么该 程序的执行看起来将是顺序一致的。如前面所提到的,在一组操作要保持原子性而 未得到保证时,即使有顺序一致性 和/或 未遭遇数据争用(当程序包含两个没有被 happens-before 关系排序的冲突访问时,就称存在数据争 用),仍然可能会出现错误。
- 假想一个情况使用
volatile
修饰的变量在多线程的进行10000
次自增操作,不会发生重排序,满足上述的顺序一致性和没有数据争用。但由于自增操作不是原子的 取值,加一,写回。在取值和加一的间隙其它线程进行 自己的自增操作,造成结果错误。
显而易见的可以知道JMM不会也不能选择顺序一致模型,因为其禁止了编译优化,造成性能上的影响。
Happens-before 模型
happens-before 内存模型描绘了一个必要而非充分的约束集。所有 Java 内存模型允 许的行为,happens-before 内存模型也允许,但是,happens-before 内存模型允许不 可接受的行为 —— 这些行为违反了我们建立的需求
规则
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
- 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;
这些规则和同步相结合保证了前一个动作对后一个动作的可视性。
太弱了
但是
Happens-before
的一致性允许一个对变量V读操作看到之前的写操作。。
- 这种比较弱的模型有一个致命的缺点,其允许值“凭空出现”.
x==y==0;
//Thread 1
r1 = x;
if (r1 != 0)
y = 1;
// Thread 2
r2 = y;
if (r2 != 0)
x = 1;
其中 r1 == r2 == 0
是JMM允许的唯一的合法行为。
然而,在 happens-before 内存模型下,存在执行结果是 r1 == r2 == 1 的情 况,允许每个读操作看到其它线程写的值(重排序带来的写操作提前执行)。
x==y==0;
//Thread 1
r1 = x;
y = r1;
// Thread 2
r2 = y;
x = r2;
会发生循环依赖,激进的编译器可能会随意赋值 r1 == r2 == 42
.同样是不被允许的。
因果关系
当一个写操作发生在了一个其依赖的读操作之前,我们将这样的问题称为因果关系,因为它涉及写操作是否会触发自身发生的问题。
从上文可以看到 Happens-before
并不能很好的处理这些问题,JMM借助因果关系处理这些问题。
- Java 内存模型将一个特定的执行过程和一个程序作为输入,然后确定该执行过程是 否是该程序的一次合法执行。它是通过逐步地建立一组“提交的”动作来实现的,这 些动作反映出了我们知道的哪些动作能够被程序执行而不需要一个“因果循环”。
当然JMM允许编译器通过一些正确的优化打破一些 ”因果循环“。为了保证不要出现因果循环可以禁止相关指令的重排。
可以看到的是 volatile
的实现保证了可见性,又禁止了相关指令的重排。