Java 内存模型
JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。
JMM 体现在以下几个方面
- 原子性 - 保证指令不会受到线程上下文切换的影响
- 可见性 - 保证指令不会受 cpu 缓存的影响
- 有序性 - 保证指令不会受 cpu 指令并行优化的影响
可见性
退不出的循环
先来看一个现象,main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:
static boolean run = true; public static void main(String[] args) throws InterruptedException { Thread t = new Thread(()->{ while(run){ // .... } }); t.start(); sleep(1); run = false; // 线程t不会如预想的停下来 }
为什么呢?分析一下:
初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。
因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率
1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
解决方法
volatile(易变关键字)
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存
可见性 vs 原子性
前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可见, 不能保证原子性,仅用在一个写线程,多个读线程的情况: 上例从字节码理解是这样的:
getstatic run // 线程 t 获取 run true getstatic run // 线程 t 获取 run true getstatic run // 线程 t 获取 run true getstatic run // 线程 t 获取 run true putstatic run // 线程 main 修改 run 为 false, 仅此一次 getstatic run // 线程 t 获取 run false
比较一下之前我们将线程安全时举的例子:两个线程一个 i++ 一个 i-- ,只能保证看到最新值,不能解决指令交错
// 假设i的初始值为0 getstatic i // 线程2-获取静态变量i的值 线程内i=0 getstatic 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
注意:
- synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized 是属于重量级操作,性能相对更低。
synchronized 关键字可以确保多线程环境下的可见性,主要通过两个方面来实现:
互斥访问:当一个线程获取到某个对象的锁时,其他线程无法同时获取该对象的锁,只能等待锁释放。这样可以保证在同步块中对共享变量的修改操作是原子的,不会被其他线程中断。
内存可见性:当一个线程释放锁时,会将对共享变量的修改刷新到主内存中,而当另一个线程获取锁时,会从主内存中重新读取共享变量的值,确保看到最新的值。
以下是一个例子来说明 synchronized 关键字如何保证可见性:
public class SynchronizedExample { private static boolean flag = false; public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(() -> { synchronized (SynchronizedExample.class) { // 修改共享变量的值 flag = true; System.out.println("Thread 1: flag is set to true"); } }); Thread thread2 = new Thread(() -> { // 暂停一段时间,确保 thread1 先执行 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (SynchronizedExample.class) { // 在同步块中访问共享变量 if (flag) { System.out.println("Thread 2: flag is true"); } else { System.out.println("Thread 2: flag is false"); } } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); } }
在这个例子中,有两个线程 thread1 和 thread2,它们同时访问了共享变量 flag。首先,thread1 获取了 SynchronizedExample.class 对象的锁,并将 flag 设置为 true,然后释放锁。接着,thread2 获取了同一个锁,并在同步块中访问 flag 的值。由于 thread2 获取到了锁并读取了 flag 的最新值,因此能正确地判断出 flag 的状态。
通过 synchronized 关键字的互斥性和内存可见性的特性,确保了多线程环境下的共享变量操作的一致性和可见性。
- 如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也能正确看到对 run 变量的修改了,想一想为什么?
static boolean run = true; public static void main(String[] args) throws InterruptedException { Thread t = new Thread(()->{ while(run){ // .... System.out.println(); } }); t.start(); sleep(1); run = false; // 线程t不会如预想的停下来 }
通过分析 System.out.println() 源码来得出结果。
synchronized 是 Java 中用于实现同步的关键字,它可以用于修饰方法和代码块。当一个线程获取了对象的锁,执行 synchronized 修饰的代码时,会将变量从主内存中拷贝到线程的本地内存中。
在 synchronized 块执行结束后,JVM 会把该线程对应的本地内存中修改过的变量刷新回主内存中,以保证不同线程间所共享的变量值的一致性。
模式之两阶段终止
// 停止标记用 volatile 是为了保证该变量在多个线程之间的可见性 // 我们的例子中,即主线程把它修改为 true 对 t1 线程可见 class TPTVolatile { private Thread thread; private volatile boolean stop = false; public void start(){ thread = new Thread(() -> { while(true) { Thread current = Thread.currentThread(); if(stop) { log.debug("料理后事"); break; } try { Thread.sleep(1000); log.debug("将结果保存"); } catch (InterruptedException e) { } // 执行监控操作 } },"监控线程"); thread.start(); } public void stop() { stop = true; thread.interrupt(); // 如果设置为ture后,线程还在sleep状态,那么使用打断即可 } }
调用
TPTVolatile t = new TPTVolatile(); t.start(); Thread.sleep(3500); log.debug("stop"); t.stop();
结果
11:54:52.003 c.TPTVolatile [监控线程] - 将结果保存 11:54:53.006 c.TPTVolatile [监控线程] - 将结果保存 11:54:54.007 c.TPTVolatile [监控线程] - 将结果保存 11:54:54.502 c.TestTwoPhaseTermination [main] - stop 11:54:54.502 c.TPTVolatile [监控线程] - 料理后事
同步模式之 Balking
定义
Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回
实现
class TwoPhaseTermination { // 监控线程 private Thread monitorThread; // 停止标记 private volatile boolean stop = false; // 判断是否执行过 start 方法 private boolean starting = false; // 启动监控线程 public void start() { synchronized (this) { if (starting) { // false return; } starting = true; monitorThread = new Thread(() -> { while (true) { Thread current = Thread.currentThread(); // 是否被打断 if (stop) { log.debug("料理后事"); break; } try { Thread.sleep(1000); log.debug("执行监控记录"); } catch (InterruptedException e) { } } }, "monitor"); monitorThread.start(); } } // 停止监控线程 public void stop() { stop = true; monitorThread.interrupt(); } } public static void main(String[] args) throws InterruptedException { TwoPhaseTermination tpt = new TwoPhaseTermination(); tpt.start(); tpt.start(); tpt.start(); /*Thread.sleep(3500); log.debug("停止监控"); tpt.stop();*/ }
当前端页面多次点击按钮调用 start 时
输出
[http-nio-8080-exec-1] cn.itcast.monitor.service.MonitorService - 该监控线程已启动?(false) [http-nio-8080-exec-1] cn.itcast.monitor.service.MonitorService - 监控线程已启动... [http-nio-8080-exec-2] cn.itcast.monitor.service.MonitorService - 该监控线程已启动?(true) [http-nio-8080-exec-3] cn.itcast.monitor.service.MonitorService - 该监控线程已启动?(true) [http-nio-8080-exec-4] cn.itcast.monitor.service.MonitorService - 该监控线程已启动?(true)
一个优化就是 尽可能的让synchronized代码块中的代码较少,所以可以将不关键的因素抽取出来
public void start() { synchronized (this) { if (starting) { // false return; } starting = true; } monitorThread = new Thread(() -> { while (true) { Thread current = Thread.currentThread(); // 是否被打断 if (stop) { log.debug("料理后事"); break; } try { Thread.sleep(1000); log.debug("执行监控记录"); } catch (InterruptedException e) { } } }, "monitor"); monitorThread.start(); }
它还经常用来实现线程安全的单例
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; } }
有序性
JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码
static int i; static int j; // 在某个线程内执行如下赋值操作 i = ...; j = ...;
可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是
i = ...; j = ...;
也可以是
j = ...; i = ...;
这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性。为什么要有重排指令这项优化呢?从 CPU执行指令的原理来理解一下吧
指令级并行原理
名词
Clock Cycle Time
主频的概念大家接触的比较多,而 CPU 的 Clock Cycle Time(时钟周期时间),等于主频的倒数,意思是 CPU 能够识别的最小时间单位,比如说 4G 主频的 CPU 的 Clock Cycle Time 就是 0.25 ns,作为对比,我们墙上挂钟的Cycle Time 是 1s
例如,运行一条加法指令一般需要一个时钟周期时间
CPI
有的指令需要更多的时钟周期时间,所以引出了 CPI (Cycles Per Instruction)指令平均时钟周期数
IPC
IPC(Instruction Per Clock Cycle) 即 CPI 的倒数,表示每个时钟周期能够运行的指令数
剑指JUC原理-8.Java内存模型(中):https://developer.aliyun.com/article/1413627