【JUC】第五章:共享模型之内存


上一章讲解的 Monitor 主要关注的是访问共享变量时,保证临界区代码的原子性。
这一章我们进一步深入学习共享变量在多线程间的【可见性】问题与多条指令执行时的【有序性】问题。

一、Java 内存模型

JMM 即 Java Memory Model,它从 Java 层面定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。JMM 体现在一下几个方面

  • 原子性:保证指令不会受线程上下文切换的影响。
  • 可见性:保证指令不回受 CPU 缓存的影响(JIT 堆热点代码的缓存优化)
  • 有序性:保证指令不会受 CPU 指令并行优化的影响。

之前讲的 synchronized 底层 Monitor 主要关注的是访问共享变量时,保证临界区代码的原子性。

1.1 可见性

当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值。

1.1.1 举个例子:退不出的循环
import static java.lang.Thread.sleep;
public class Test1 {
    private static boolean run = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (run) {

            }
        });
        t.start();
        sleep(1000);
        run = false;
    }
}

为什么会发生这种情况,我们分析一下:

  1. 初始状态下,t 线程刚开始从内存读取了 run 的值到工作内存。

  1. 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率。

  1. 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取了这个变量的值,结果永远是旧值。


可见性问题,这里涉及到 Java 内存模型(JMM)
JMM 描述了 Java 程序中各种变量(线程共享变量)的访问规则,以及在 JVM 中将变量存储到内存和从内存中读取出变量这样的底层细节。
在内存模型中,所有的变量都存储在主内存中。每个线程都有自己独立的工作内存,里面保存着该线程使用到的变量的副本。
内存模型图:

JMM 线程操作内存的两条基本规定:

  1. 关于线程与主内存:线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写。
  2. 关于线程间工作内存:不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值的传递需要经过主内存来完成。

共享变量可见性实现的原理:
线程 1 对共享变量的修改要想被线程 2 及时看到,必须要经过两个步骤:

  1. 把工作内存 1 中更新过的共享变量刷新到主内存中
  2. 把内存中最新的共享变量的值更新到工作内存 2 中。

1.1.2 解决方法
  • 使用 volatile(表示易变关键字的意思),它可以用来修饰 成员变量静态成员变量,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存。
  • 使用synchronized 关键字也有相同的效果,在Java内存模型中,synchronized规定,线程在加锁时,需要先清空工作内存 -> 在主内存中拷贝最新变量的副本到工作内存 -> 执行完代码 -> 将更改后的共享变量的值刷新到主内存中 -> 释放互斥锁
1.1.3 可见性 vs 原子性

前面例子体现的实际就是可见性,它是指保证多个线程之间一个线程对volatile 变量的修改对另一个线程可见,而不能保证原子性。volatile用在一个写线程,多个读线程的情况,比较合适。
举个例子:两个线程一个i++,一个 i–,但是volatile只保证可见性,只能看到最新之,但不能解决指令交错问题(原子性)。

// 假设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 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是它是属于重量级操作,性能相对较低。如果在前面实例的死循环中加入 System.out.println() 会发现即使不加volatile修饰符,线程t也能正确看到对run变量的修改了,想想是为什么?因为println方法里面有synchronized修饰。

1.1.4 模式之两阶段种植

我们之前的做法:当我们在执行线程一时,想要终止线程二,这就需要使用interrupt 方法来优雅的停止线程二。
使用volatile 关键字来实现两阶段终止模式。

public class Test1 {
    public static void main(String[] args) throws InterruptedException {

        // 下面是两个线程操作共享变量stop
        Monitor monitor = new Monitor();
        monitor.start();

        Thread.sleep(3500);
        monitor.stop();
    }
}


class Monitor {

    // private boolean stop = false; // 不会停止程序
    private volatile boolean stop = false; // 会停止程序

    /**
     * 启动监控器线程
  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值