JMM 即 Java Memory Model , 它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。
JMM体现在以下几个方面
现象
main线程对于变量的修改对于t线程是不可见的,导致了 t 线程无法停止
@Slf4j
public class HasSeeTest {static boolean hasExit = false;public static void main(String[] args) {new Thread(() -> {while (!hasExit){log.debug("循环中,等待hasExit为true");}}).start();sleep(1);log.debug("hasExit修改true");// 线程t并没有结束hasExit = true;}
}
分析:
因为t线程要频繁从主内存中频繁读取hasExit的值,JIT编译器会将hasExit的值缓存至自己工作内存中的高速缓存中,减少对主内存中hasExit的访问,提高效率
1秒之后,main修改了hasExit的值,并同步到主存,而t是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
volatile(易变关键字)
开始的例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对volatile 变量的修改对另一个线程可见,不能保证原子性,仅用在一个写线程,多个读线程; 字节码理解:
getstatic hasExit // 线程t获取 hasExit false
getstatic hasExit // 线程t获取 hasExit false
getstatic hasExit // 线程t获取 hasExit false
getstatic hasExit // 线程t获取 hasExit false
getstatic hasExit // 线程t获取 hasExit false
putstatic hasExit // 线程 main 修改hasExit为 true,仅此一次修改
getstatic hasExit // 线程t获取 hasExit true
比较之前线程安全:两个线程一个 i++ 一个 i–,只能保证看到最新值,不能解决指令交错
// i的初始值为0
getstatic i // 线程2 获取i的值 线程内i=0getstatic i // 线程1 获取i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1 iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1
结论:
读取数据流程如下
MESI 协议
E、S、M 状态的缓存行都可以满足CPU的读请求
E 状态的缓存行,有些请求,会将状态改为M,这时并不触发向主存的写
E 状态的缓存行,必须监听该缓存行的读操作,如果有,要变为S 状态
M 状态的缓存行,必须监听该缓存行的读操作,如果有,先将其他缓存(S 状态)中该缓存行变成 I 状态(即6的流程),写入主存,自己变为S状态
S 状态的缓存行,必须监听该缓存行的失效操作,如果有,自己变为I状态
S状态的缓存行,必须监听该缓存行的失效操作,如果有,自己变为I状态
I 状态的缓存行,有读请求,必须从主存读取
Memory Barrier(Memory Fence)
可见性
有序性
Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做 了,直接结束返回
@Slf4j
public class MonitorTest {// 用来表示是否已经有线程已经在执行启动了private static volatile boolean starting = false;public static void main(String[] args) {new Thread(() -> {while(!starting){log.debug("监控线程是否启动");}log.debug("监控线程启动");}).start();Sleeper.sleep(1);log.info("尝试启动监控线程....");synchronized (MonitorTest.class){starting = true;}}
}
还可以用实现线程安全的单例
@Slf4j
public final class Singleton {private Singleton() {}private static Singleton INSTANCE = null;public static synchronized Singleton getInstance() {if (INSTANCE != null) {return INSTANCE;}INSTANCE = new Singleton();return INSTANCE;}
}
volatile 修饰的变量,可以禁用指令重排
volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
@Actorpublic void actor2(I_Result r) {num = 2;ready = true;}
@Actorpublic void actor1(I_Result r) {if(ready) {r.r1 = num + num;} else {r.r1 = 1;}}
@Actorpublic void actor2(I_Result r) {num = 2;ready = true; // ready是 volatile 赋值带写屏障// 写屏障}
@Actorpublic void actor1(I_Result r) {// 读屏障// ready 是 volatile 读值带读屏障if(ready) {r.r1 = num + num;} else {r.r1 = 1;}}
不能解决指令交错
双重检测锁最熟知的就是 懒汉式单例
@Slf4j
public final class Singleton {private Singleton() {}private static Singleton INSTANCE = null;public static Singleton getInstance() {if (INSTANCE == null) {// 首次访问会同步 而之后的使用没有synchronizedsynchronized (Singleton.class){if (INSTANCE == null){INSTANCE = new Singleton();}}return INSTANCE;}return INSTANCE;}
}
但在多线程下,是有问题的,getInstance 方法对应的字节码为:
0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
3: ifnonnull 37
6: ldc #3 // class cn/itcast/n5/Singleton
8: dup
9: astore_0
10: monitorenter
11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
14: ifnonnull 27
17: new #3 // class cn/itcast/n5/Singleton
20: dup
21: invokespecial #4 // Method "":()V
24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
27: aload_0
28: monitorexit
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
40: areturn
jvm会有优化为:先执行24,在执行21.如果两个线程t1、t2,按如下时间序列执行:
关键在于
@Slf4j
public final class Singleton {private Singleton() {}private static volatile Singleton INSTANCE = null;public static Singleton getInstance() {if (INSTANCE == null) {// 首次访问会同步 而之后的使用没有synchronizedsynchronized (Singleton.class){if (INSTANCE == null){// 也许有其它线程已经创建实例,所以再判断一次INSTANCE = new Singleton();}}return INSTANCE;}return INSTANCE;}
}
// -------------------------------------> 加入对 INSTANCE 变量的读屏障
0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
3: ifnonnull 37
6: ldc #3 // class cn/itcast/n5/Singleton
8: dup
9: astore_0
10: monitorenter -----------------------> 保证原子性、可见性
11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
14: ifnonnull 27
17: new #3 // class cn/itcast/n5/Singleton
20: dup
21: invokespecial #4 // Method "":()V
24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
// -------------------------------------> 加入对 INSTANCE 变量的写屏障
27: aload_0
28: monitorexit ------------------------> 保证原子性、可见性
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
40: areturn
为什么要有重排指令这项优化呢?从 CPU 执行指令的原理出发
Clock Cycle Time
CPI
IPC
CPU执行时间
程序的 CPU 执行时间,即我们前面提到的 user + system 时间,可以用下面的公式来表示
程序CPU 执行时间 = 指令数 * CPI * Clock Cycle Time
加工一条鱼需要 50 分钟,只能一条鱼、一条鱼顺序加工…
可以将每个鱼罐头的加工流程细分为 5 个步骤:
即使只有一个工人,最理想的情况是:他能够在 10 分钟内同时做好这 5 件事,因为对第一条鱼的真空装罐,不会 影响对第二条鱼的杀菌出锅…
事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令。为什么这么做呢?可以想到指令 还可以再划分成一个个更小的阶段,例如,每条指令都可以分为: 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据 写回 这 5 个阶段
在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行,这一技术在 80世纪 中 叶到 90世纪 中叶占据了计算架构的重要地位。
注意
奔腾四(Pentium 4)支持高达 35 级流水线,但由于功耗太高被废弃
大多数处理器包含多个执行单元,并不是所有计算功能都集中在一起,可以再细分为整数运算单元、浮点数运算单元等,这样可以把多条指令也做到并行获取、译码等,CPU可以在一个时钟周期内,执行多于一条指令,IPC>1