Java 多线程内存模型跟cpu 缓存模型类似,是基于cpu缓存模型来建立的,Java 内存模型是标准化的,屏蔽掉了底层不同计算机的区别。
共享变量都是放在主内存中进行存储,举个例子:我们现在有一个共享变量 a = 1,多线程对a 进行操作,实际操作是将共享变量分别复制到自己线程的工作内存中(共享变量副本),操作完之后会把修改后的值刷回主内存。
代码案列:
package com.sinosoft.gov.business.back.LbNewsFlash.controller;
public class VolatileVisibilityTest {
private static 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(1000);
new Thread(() -> prepareData()).start();
}
public static void prepareData() {
System.out.println("prepare data");
initFlag = true;
System.out.println("prepare data end ......");
}
}
打印结果:
程序并没有跳出循环,一直处于运行状态。
package com.sinosoft.gov.business.back.LbNewsFlash.controller;
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(1000);
new Thread(() -> prepareData()).start();
}
public static void prepareData() {
System.out.println("prepare data");
initFlag = true;
System.out.println("prepare data end ......");
}
}
加上 volatile 程序就可以达到预期效果,运行结果:
waiting data.....
prepare data
prepare data end ......
====== success
为什么加了 volatile 就可以达到预期的效果,它的底层到底是怎么实现的呢?
JMM 数据原子操作
read (读取) | 从主内存读取数据 |
load (载入) | 将主内存读取到的数据写入工作内存 |
use (使用) | 从工作内存读取数据来计算 |
assign (赋值) | 将计算好的值重新赋值到工作内存中 |
store (存储) | 将工作内存数据写入主内存 |
write (写入) | 将store过的变量值赋值给驻内存中的变量 |
lock (锁定) | 将主内存变量加锁,标识为线程独占状态 |
unlock (解锁) | 将主内存变量解锁,解锁后其他线程可以锁定该变量 |
JMM缓存不一致问题
缓存一致性协议(MESI)
多个cpu从主内存读取同一个数据到各自的高速缓存,当其中某个cpu 修改了缓存里的数据,第一步:该数据会马上同步回主内存(而不是等这个线程全部执行完),第二步:其cpu 通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效。
缓存加锁
缓存锁的核心机制是基于缓存一致性协议来实现的,一个处理器的缓存会回写到内存会导致其他处理器的缓存失效,IA-32 和Intel 64 处理器使用MESI 实现缓存一致性协议。
MESI协议缓存状态
状态 | 描述 | 监听任务 |
M 修改(Modified) | 该Cache line 有效,数据被修改了,和内存中的数据不一致,数据只存在于Cache中 | 缓存行必须时刻监听所有试图读该缓存行相对就主存的缓存行写回主内存并将状态变成S(共享)状态之前被延迟。 |
E 独享、互斥(Exclusive) |
该Cache line有效,缓存行内容和内存中的一样,数据只存在于本Cache中。 | 缓存行也必须监听其他缓存读主存中该缓存行的操作,变成S (共享状态) |
S 共享(Shared) | 该Cache line有效,数据和内存中的数据一致,数据存在于很多Cache中。 | 缓存行也必须监听其他缓存使该缓存行无效或者独享该 |
I 无效 (Invalid) | 该Cache line 无效 | 无 |
volatile 修饰的成员变量,每次被线程访问时,都强制从共享内存中重新读取一次该成员变量的值,但成员变量的值发生改变时,会强制将成员变量的值回写到共享内存中,这样,两个不同的线程看到的是同一个值。
volatile 为什么不能保证原子性?
问题来了,既然他可以保证修改的值立即能更新到主存,其他线程也会捕捉到被修改后的值,那么为什么不能保证原子性呢?首先需要了解的是,Java 中只有对基本类型变量的赋值和读取是原子操作,如 i = 1 的赋值操作,但是 j = i 或者 i++ 这样的操作都不是原子操作,因为他们都进行了多次原子操作,比如先读取 i的值。再将 i 的值 赋值给j,两个原子操作加起来就不是原子操作。