并发5️⃣内存①JMM

1、Java 内存模型

Java Memory Model

作用让 Java 程序在所有平台具有一致的内存访问效果,而不受硬件和操作系统影响

  • JMM 定义了程序中各种变量的访问规则,体现为主存工作内存两个结构。
  • 特性:原子性、可见性、有序性。

1.1、主存 & 工作内存

所有变量都存储在主存中。

每个线程有自己的工作内存,其中保存了该线程所使用变量的主存副本。

通常情况下

  • 线程对变量的读写操作只在工作内存中进行,不直接读写主内存中的数据。

  • 线程之间也无法直接访问对方工作内存的变量,需要通过主内存来传递

    image-20220306150024740

1.2、三大特性(❗)

解决问题 实现方式
原子性 线程上下文切换 Lock 或 synchronized
可见性 CPU 缓存 volatile 或 synchronized
有序性 CPU 指令并行优化 volatile

原子性:多行字节码指令能同时执行,不受线程上下文影响。

解决:synchronizedLock

2、可见性

可见性主存中对变量的修改,对工作内存可见

即线程读取变量的最新值,而非缓存的旧值。

2.1、问题描述

2.1.1、退不出的循环

① 示例

循环条件:run

  1. 线程 t1 进入循环,循环条件为 run == true

  2. 一秒后,主线程设置 run = false

  3. 线程 t1 没有退出循环

    static boolean run = true;
    
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (run) {
                // ....
            }
        }, "t1").start();
    
        SleepUtils.sleepSeconds(1);
        run = false;
    }
    

② 分析

简单来说:工作内存没有感知到主存的变量已修改

  1. 线程 t1 刚开始运行时,读取主存中的 run 值。

  2. 线程 t1 执行一段时间后,JIT 编译器while 循环热点代码)优化,将主存中的 run 值缓存到工作内存

  3. 主线程修改主存中的 run 值,但 t1 读工作内存中的 run 值。

image-20220425004613043

2.1.2、解决方法(❗)

原理:两种解决方案

  • volatile:要求线程从主存中读取变量,避免线程从工作缓存中读取。
  • synchronized
    1. 线程执行到 synchronized 同步代码块,加锁
    2. 清空工作内存,将主存中的变量值拷贝到工作内存。
    3. 执行完同步代码块,将更新后的变量值刷新到主存中
    4. 释放互斥锁

对比

volatile synchronized
含义 易变 同步
修饰变量 成员变量 成员变量
解决问题 可见性 可见性、原子性
说明 无法解决原子性问题
(即不能防止线程之间的指令交错)
性能低:涉及底层的 Monitor 结构

2.2、模式:两阶段终止

实现方式

  1. isInterrupted()
  2. volatile

2.2.1、回顾 isInterrupted()

案例:后台监控

① 图示

循环

  1. 判断中断标志:即 isInterrupted(),判断是否被中断。

    • true:处理后事(如保存结果、释放锁等),结束线程。
    • false:执行任务。
  2. 执行任务:执行结束后睡眠 1 秒捕获睡眠期间的异常

    • 正常:进入下一轮循环。

    • 异常:睡眠线程被中断会清除中断标志,因此需要手动重新设置中断标志,进入下一轮循环。

      image-20220323140958368

② 代码实现

  1. 判断中断标志:若被中断则处理后事并结束进程。

  2. 执行任务:执行结束后睡眠 1 秒,捕获睡眠期间的异常。

  3. 睡眠期间被中断:中断标志被清除,需要重新设置中断标志

    private Thread thread;
    
    public void turnOn() {
        thread = new Thread(() -> {
            Thread current = Thread.currentThread();
            while (true) {
                if (current.isInterrupted()) {
                    // 模拟:处理后事
                    LogUtils.debug("terminating...");
                    // 处理完毕后退出
                    LogUtils.debug("Exit");
                    break;
                }
                // 执行任务
                LogUtils.debug("working...");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    // 设置中断标志
                    current.interrupt();
                    e.printStackTrace();
                }
            }
        }, "t");
        thread.start();
    }
    
    public void turnOff() {
        thread.interrupt();
    }
    

2.2.2、volatile(❗)

相比 isInterrupter() 方式,使用了自定义中断标志

  • 启动
    1. 自定义 stopFlag 变量作为中断标志volatile 保证可见性
    2. 睡眠期间被中断,不需要重设 interrupted 中断标志
  • 终止
    1. 将 stopFlag 中断标志设为 true
      (相当于 isInterrupted() 方式的 interrupt()
    2. interrupt() 强制中断线程
      (若没有该方法,t1 在睡眠期间被中断时需等待睡眠时间结束)

代码

private volatile boolean stopFlag;

public void turnOn() {
    thread = new Thread(() -> {
        while (true) {
            if (stopFlag) {
                // 模拟:处理后事
                LogUtils.debug("terminating...");
                // 处理完毕后退出
                LogUtils.debug("Exit");
                break;
            }
            // 执行任务
            LogUtils.debug("working...");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                // 不需要设置中断标志
                e.printStackTrace();
            }
        }
    }, "t1");

    thread.start();
}

public void turnOff() {
    LogUtils.debug("stop");
    stopFlag = true;
    thread.interrupt();
}

2.3、同步模式:Balking

2.3.1、定义

犹豫(Balking),即懒加载

  • 定义:线程执行某个任务时发现任务已被执行(当前线程 / 其它线程),则当前线程无需再次执行,直接返回
  • 应用:懒汉式单例模式

2.3.2、案例

模拟 web 环境启动监控功能

定义 startFlag 标识是否已启动,volatile 保证可见性。

  • 启动

    1. 判断 startFlag,已启动则直接返回,未加锁则设置 true(必须保证原子性
    2. 启动监控
  • 说明:在 web 环境中,点击一次页面的【启动】按钮,相当于调用一次 turnOn() 方法。

    public class BalkingModel {
        private volatile boolean startFlag;
    
        public void turnOn() {
            synchronized (this) {
                if (startFlag) {
                    LogUtils.debug("已开启监控");
                    return;
                }
                startFlag = true;
            }
            // 模拟启动监控
            LogUtils.debug("监控中");
        }
    }
    

3、有序性

有序性:字节码指令按一定顺序被 CPU 调度执行。

指令重排:JVM 调整字节码指令的执行顺序,以提高代码的整体执行效率。

3.1、指令重排

JVM 运行期优化技术

通常是将易于执行的指令(如资源耗费低)重排序到前面。

  • 单线程:可以提高代码整体执行效率。将易于执行的指令先执行
  • 多线程:会影响正确性

3.2、示例:诡异的结果

两个线程分别执行以下两个方法,问 result 的最终结果?

int result = 0;

int num = 1;
boolean ready = false;

// 线程t1执行
public void m1() {
    if(ready) {
        // ①
        result = num + num;
    } else {
        // ②
        result = 1;
    }
}
// 线程t2执行
public void m2() {
    // ③
    num = 2;
    // ④
    ready = true;
}

3.2.1、分析

① 正常情况

有 3 种情况:结果可能是 1 或 4。

  1. 先进入 m1():条件假,进入 else 执行 ②

    image-20220306154058047

  2. 先进入 m2():执行③,④还未执行;此时进入 m1():条件假,执行②

    image-20220306154156466

  3. 先进入 m2():执行③④;再进入 m1():条件真,执行①

    image-20220306154347866

② 指令重排

还有一种情况:result == 2

  1. 先进入 m2():执行④,③未执行

  2. 此时进入 m1():条件真,执行①

image-20220306154849717

3.2.2、解决:volatile

使用 volatile 修饰成员变量,可保证 ready。

posted @ 2022-04-25 01:21  Jaywee  阅读(5)  评论(0编辑  收藏  举报

👇