1. 特性
- 有序性:程序执行的顺序按照代码的先后顺序执行。通过内存屏障实现
- 可见性:多个线程访问同一个变量时,一个线程修改了值,其他线程能够立即看到修改的值。通过将变量存放至主内存,使用
MESI缓存一致性协议实现 - 不保证原子性:一个或多个操作,要么全部执行要么全部都不执行
2. 有序性
2.1 背景
在多线程环境下,JVM会对指令进行重排序以优化程序执行性能。这种重排序有可能导致不同线程之间对共享变量操作的有序性变得不确定,进而引发并发问题。
JVM的指令重排序发生在编译器和处理器层面上,它们为了提高指令执行效率,在不影响单线程程序执行结果的前提下(即遵循as-if-serial语义),可能会调整代码中看似无关的操作顺序。然而,在多线程场景下,一个线程中的指令重排序如果影响到了其他线程对相同数据的读写顺序,那么就可能破坏了这些线程间的可见性和有序性保证,从而产生难以预料的行为。
为了解决这个问题,Java内存模型(Java Memory Model, JMM)引入了happens-before原则以及使用volatile、synchronized等同步机制来确保多线程环境下的有序性和可见性
2.2 hanppens-before原则
如果两个操作的执行顺序无法通过此原则推导,则不能保证有序性,可以随意进行重排序
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作,happens-before 于书写在后面的操作
- 锁定规则:一个
unLock操作,happens-before 于后面对同一个锁的 lock 操作 - volatile 变量规则:对一个变量的写操作,happens-before 于后面对这个变量的读操作
- 传递规则:如果操作 A happens-before 操作 B,而操作 B happens-before 操作C,则可以得出,操作 A happens-before 操作C
- 线程启动规则:Thread 对象的 start 方法,happens-before 此线程的每个一个动作
- 线程中断规则:对线程 interrupt 方法的调用,happens-before 被中断线程的代码检测到中断事件的发生
- 线程终结规则:线程中所有的操作,都 happens-before 线程的终止检测。可以通过
Thread.join()方法结束、Thread.isAlive()的返回值手段,检测到线程已经终止执行 - 对象终结规则:一个对象的初始化完成,happens-before 它的
finalize()方法的开始
2.3 有序性实现原理
在volatile变量读写操作前后,插入内存屏障
- 在volatile写前,插入
StoreStore屏障,禁止处理器对同一变量的两个连续store操作直之间进行重排序,保证本次写操作之前的写操作结果,都已经刷新到主内存 - 在volatile写后,插入
StoreLoad屏障,禁止处理器将store操作与后续的load操作进行重排序,保证本次写操作,一定优先于后续的读操作 - 在volatile读前,插入
LoadLoad屏障,禁止处理器对同一变量的两个连续load操作之间进行重排序,保证本次读操作之前的读操作一定先执行 - 在volatile读后,插入
LoadStore屏障,禁止处理器将load操作与后续的store操作进行重排序,保证本次读操作,一定优先于后续的写操作
实际上,只要不改变 volatile 写-读的内存语义,编译器可以根据具体情况优化,省略不必要的屏障
3. 可见性
当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值,立即刷新到主内存中。
当读一个 volatile 变量时,JMM 会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量。
3.1 实现原理:MESI缓存一致性协议
- Modified(已修改):代表数据已经被更新过,还未写入主内存
- Exclusive(独占):代表数据只有当前核心持有,可以自由写入而不通知,无一致性问题
- Shared(共享):代表数据被多个核心持有,更新数据时需要先广播通知,要求其他核心将该数据标记为【已失效】状态,然后再更新当前数据
- Invalidated(已失效):代表当前数据已失效,不可以读取或者更新
优点:在缓存状态为Modified/Exclusive时,更新其数据不需要发送广播,减少总线宽带压力
4. 总结
volatile关键字修饰的变量,通过内存屏障保证了一定的有序性,通过将变量存储到主内存中和MESI缓存一致性协议实现了可见性。