java并发编程(十三)java内存共享模型原理分析

「这是我参与2022首次更文挑战的第25天,活动详情查看:2022首次更文挑战

一、java内存模型

JMM(Java Memory Model),它定义了主存、工作内存的概念,底层同时对应着CPU的主存,缓存,寄存器,硬件等。

前面的文章简单提到过,如下图所示:

image.png 而JMM有以下几个重点:

  • 原子性:线程上下文不会影响指令结果
  • 可见性:CPU缓存不影响指令结果
  • 有序性:CPU并行优化(指令重排序)不会影响指令结果

二、可见性

首先看如下的例子:

public class JMMTest {

    static Boolean state = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
           while(state){

           }
        },"t1").start();

        TimeUnit.MILLISECONDS.sleep(1000);
        state = false;
    }
}
复制代码

结果将导致t1线程无法停止,因为main方法修改成员变量state,对于线程t1是不可见的。

那么为什么会发生上述的问题?逐步分析一下:

  • 主方法启动,静态变量state被加载到主内存当中,其值是true。
  • 线程t1执行,从主内存加载state到线程私有内存(工作内存),值是true。
  • 上述代码会不停的到主内存当中获取state值,所以JIT编译器(即时编译器)将state值缓存到自己的高速缓存当中。用以减少对主内存的访问。
  • 当主线程在一秒后修改state为false时,修改的是主内存的值,而t1使用的是工作内存当中高速缓存中的值,所以仍然是true。

那么有什么解决方案呢?

使用volatile关键字。

volatile关键字可以修饰成员变量和静态变量。它能够使线程到主内存中获取值,避免去缓存当中取值。这就是可见性

public class JMMTest {

    static volatile Boolean state = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
           while(state){

           }
        },"t1").start();

        TimeUnit.MILLISECONDS.sleep(1000);
        state = false;
    }
}
复制代码

注意:System.out.println将影响线程的可见性

如果代码如下所示:

public class JMMTest {

    static Boolean state = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
           while(state){
               System.out.println("t1运行中");
           }
        },"t1").start();

        TimeUnit.MILLISECONDS.sleep(1000);
        state = false;
    }
}
复制代码

将不会出现我们预想的无法停止的情况。

原因,我们看println的源码发现,使用了synchronized关键字。

我们在前面学习synchronized的时候,学习过Monitor(监视器或管程),重量级锁需要使用Monitor去存储对象头的信息,同时其中的Owner会记录当前持有锁的线程。有且只能有一个线程持有锁,知道其退出,才可以有其他线程持有。

同步不仅仅表示互斥:这点在前面学习synchronized时没有介绍,其同样具有可见性的语义。

  • 其他线程持有Monitor时:

    • 获取Monitor,本地缓存失效,重新从主内存加载数据。
  • 线程退出Monitor时:

    • 释放Monitor,将线程私有内存(即缓存)刷新到主内存

结论:synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized 是属于重量级操作,性能相对更低。

经过以上简单解释,就能基本阐述这个问题的原因了。

三、原子性

这里不做过多解释,凡是涉及到多线程的地方都会有原子性的问题。

我们在前面文章学习的synchronized,就是通过互斥的线程同步方式,保证变量的原子性。

而在上一个小节提到的volatile关键字只能保证线程间的可见性,不能保证原子性。

除了synchronized之外,ReetrantLock、LockSupport等等可以保证原子性。

以及juc下面的很多类都是原子性的,后面的文章都会学到。

总而言之,java内存模型决定着,保证原子性是必要的也是必然的。

四、有序性

CPU(JVM)在不影响指令结果的前提下,会优化指令执行的顺序,但是在多线程的场景下,这有可能会造成问题,有些场景下我们是需要避免指令重排序的。

那么CPU为什么要做指令重排序优化呢?后面的文章会详解,本文只做入门理解。

如何禁止指令重排序?

答案同样是使用volatile,由此可见volatile的重要性

happens-before原则

关于有序性的内容,其实有一个很著名的原则:happens-before原则

其规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见。


关于volatile的原理,在下一篇文章我会单独去讲解。

猜你喜欢

转载自juejin.im/post/7063256149562720264