以下是结合代码,以流程图形式解析JMM数据原子操作:
示例代码
public class VolatileVisibilityTest {
private static volatile boolean initFlag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("waiting data...");
while (!initFlag) {
System.out.println("===================success");
}
}).start();
Thread.sleep(2000);
new Thread(() -> prepareData()).start();
}
public static void prepareData() {
System.out.println("prepare data...");
initFlag = true;
//TODO
System.out.println("prepare data end...");
}
}
流程图解析
解析说明
- 开始操作:线程开始对共享变量
sharedVariable
进行操作。 - 锁定变量:线程尝试对主内存中的
sharedVariable
加锁(lock
操作),以标识该变量为线程独占状态。如果加锁失败,线程会不断尝试,直到成功获取锁。 - 读取数据:获取锁成功后,线程从主内存读取
sharedVariable
的值(read
操作)。 - 载入数据:将从主内存读取到的值写入线程的工作内存(
load
操作)。 - 使用数据:从工作内存中读取
sharedVariable
的值,并将其赋值给局部变量localValue
(use
操作)。 - 计算数据:对局部变量
localValue
进行计算,例如localValue = localValue + 1
。 - 赋值数据:将计算后的结果
localValue
重新赋值到工作内存中的sharedVariable
(assign
操作)。 - 存储数据:将工作内存中的
sharedVariable
数据准备写入主内存(store
操作)。 - 写入数据:将
store
操作的数据真正赋值给主内存中的sharedVariable
(write
操作)。 - 解锁变量:完成对共享变量的操作后,线程对主内存中的
sharedVariable
解锁(unlock
操作),其他线程此时可以竞争获取该变量的锁并进行操作。 - 结束操作:线程结束对共享变量的操作。
通过上述一系列操作,确保了在多线程环境下对共享变量的操作具有原子性,避免了数据竞争和不一致问题。虽然在这个简单示例中,Java 对基本数据类型的简单读写本身具有原子性,但上述流程展示了完整的 JMM 数据原子操作逻辑,对于更复杂的操作(如复合操作),这种原子操作流程更为关键。
补充:
JMM数据原子操作
read(读取):从主内存读取数据
load(载入):将主内存读取到的数据写入工作内存
use(使用):从工作内存读取数据来计算
assign(赋值):将计算好的值重新赋值到工作内存中
store(存储):将工作内存数据写入主内存
write(写入):将store过去的变量值赋值给主内存中的变量
lock(锁定):将主内存变量加锁,标识为线程独占状态
unlock(解锁):将主内存变量解锁,解锁后其他线程可以定该变量
- JMM缓存不一致问题根源
- 在多处理器系统中,每个处理器都有自己的高速缓存(Cache)。当多个处理器同时从主内存读取同一个数据到各自的高速缓存时,就可能出现缓存不一致的情况。例如,多个线程在不同处理器上运行,它们都访问并可能修改共享变量。如果没有合适的机制,一个处理器对共享变量的修改可能不会及时反映到其他处理器的缓存中,导致各个处理器缓存中的数据不一致,进而使程序出现错误的运行结果。
- 缓存一致性协议(MESI)
- 协议概述:MESI是一种广泛应用于多核处理器系统的缓存一致性协议,它的名称来自于该协议中缓存行(Cache Line)可能处于的四种状态:修改(Modified)、独占(Exclusive)、共享(Shared)和无效(Invalid)。
- 工作原理:
- 修改(Modified):当一个处理器修改了其缓存中的数据,该缓存行处于修改状态。此时,此缓存行的数据与主内存中的数据不同,并且只有该处理器的缓存中有最新数据。在合适的时机,该处理器会将修改后的数据写回主内存。例如,在一个多线程程序中,线程A在处理器P1上运行,它修改了共享变量
x
,此时处理器P1中包含x
的缓存行就处于修改状态。 - 独占(Exclusive):如果一个缓存行处于独占状态,意味着该缓存行中的数据与主内存一致,并且只有当前处理器的缓存中有这个数据。当有其他处理器读取相同数据时,该缓存行状态会变为共享状态。例如,线程B在处理器P2上首次读取共享变量
y
,如果此时没有其他处理器缓存y
,则处理器P2中包含y
的缓存行处于独占状态。 - 共享(Shared):多个处理器的缓存中都可能有这个缓存行的数据,且这些数据与主内存一致。当其中一个处理器修改了共享状态的缓存行数据时,其他处理器的相同缓存行状态会变为无效。比如,线程C和线程D分别在处理器P3和P4上同时读取共享变量
z
,此时处理器P3和P4中包含z
的缓存行都处于共享状态。若线程C修改了z
,处理器P3中z
的缓存行变为修改状态,而处理器P4中z
的缓存行变为无效状态。 - 无效(Invalid):表示该缓存行的数据已经无效,需要从主内存重新读取。当一个处理器收到其他处理器发出的缓存行数据已修改的通知(通过总线嗅探机制)时,会将自己缓存中相应的缓存行标记为无效。例如,处理器P4在收到处理器P3修改了共享变量
z
的通知后,将其缓存中包含z
的缓存行标记为无效。
- 修改(Modified):当一个处理器修改了其缓存中的数据,该缓存行处于修改状态。此时,此缓存行的数据与主内存中的数据不同,并且只有该处理器的缓存中有最新数据。在合适的时机,该处理器会将修改后的数据写回主内存。例如,在一个多线程程序中,线程A在处理器P1上运行,它修改了共享变量
- 总线嗅探机制:这是MESI协议实现的关键机制之一。每个处理器通过监听系统总线,嗅探其他处理器对缓存行的操作。当一个处理器修改了其缓存中的数据并写回主内存时,其他处理器通过总线嗅探到这个变化,从而将自己缓存中对应的缓存行标记为无效。这样就保证了各个处理器缓存中的数据与主内存数据的一致性。
- 缓存加锁
- 核心机制:缓存锁基于缓存一致性协议(如MESI)来实现。当一个处理器要对某个共享数据进行操作时,它首先尝试获取缓存锁。如果成功获取,它可以对缓存中的数据进行修改。由于缓存一致性协议的存在,当这个处理器修改了缓存中的数据并写回主内存时,会导致其他处理器的缓存无效。例如,在IA - 32和Intel 64处理器中,使用MESI协议实现缓存一致性。当一个处理器获取缓存锁并修改了共享数据后,其他处理器通过总线嗅探机制感知到数据变化,将自己缓存中相应的数据标记为无效,从而避免了多个处理器同时修改同一数据导致的数据不一致问题。
- 与JMM的关系:JMM(Java内存模型)虽然是Java层面的抽象概念,但底层的实现依赖于硬件层面的缓存一致性机制,如MESI协议以及缓存加锁机制。JMM通过定义一系列规则,保证了Java程序在多线程环境下的内存可见性、原子性和有序性,而硬件层面的缓存一致性机制是实现这些特性的基础。例如,JMM中对
volatile
变量的可见性保证,在硬件层面可能就是通过缓存一致性协议来实现的。当一个线程修改了volatile
变量,这个修改会通过缓存一致性协议及时传播到其他线程所在处理器的缓存中,保证其他线程能及时看到最新值。
综上所述,缓存一致性协议(如MESI)和缓存加锁机制在解决多处理器系统中缓存不一致问题上起着关键作用,同时也是JMM实现其内存模型特性的重要硬件基础。
汇编代码查看:
javap -c Xxxx.class
汇编包:
hsdis-amd64.dylib
汇编vm参数:
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly