深入理解 Java 内存模型 JMM 与 volatile

2020-04-07  本文已影响0人  张贤同学

Java 内存模型(Java Memory Model,简称 JMM)是一种抽象的概念,并不真实存在,它描述的是一组规范或者规则,通过这种规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式。

JMM 中的主内存和工作内存

由于代码运行的实体是线程,而 JVM 会为每一个线程创建一个工作内存(有些资料称为栈空间),用于存储线程私有的数据。而 Java 内存模型规定所有变量都存储在主内存中。主内存是共享内存区域,所有线程都可以访问。但是线程不能直接操作主内存中的变量,线程对变量的读取和修改等操作必须在自己的工作内存中进行,首先从主内存中把变量拷贝到线程私有的工作内存,对变量进行操作后,再将变量写回主内存。线程之间的传值必须通过主内存完成。
<div align="center"> <img src="https://user-gold-cdn.xitu.io/2020/4/7/171554d44b4eba79?w=823&h=446&f=png&s=29990"/> </div>

JMM 中的主内存

JMM 中的主内存存储 Java 实例对象,包括成员变量、类信息、常量、静态变量等。主内存属于数据共享区域,多线程并发操作时会引发线程安全问题

JMM 中的工作内存

JMM 与 Java 内存区域的划分是不同的概念层次

JMM 描述的是一组规则,通过这组规则控制程序中各个变量在主内存和工作内存访问方式,围绕原子性、有序性、可见性展开。

JMM 与 Java 内存区域划分的相似点是都存在共享区域和私有区域。
JMM 中的主内存属于共享数据区域,应该包括
Java 内存区域中的堆和方法区;JMM 中的工作内存属于私有数据区域,应该包括
Java 内存区域中的程序计数器、虚拟机栈和本地方法栈。

主内存与工作内存的数据存储类型以及操作方式归纳

JMM 如何解决可见性问题

忽略硬件中其他复杂的因素,上面的主内存与工作内存执行方式可以理解为
把数据从内存加载到CPU 的寄存器,操作完成之后再写回主内存。在现代多核 CPU 的情况下,线程共享变量就有可能出现不一致,如果运行在 CPU A 上的线程对某个变量进行了修改,而运行在其他 CPU 运行的线程加载的是 CPU 缓存中的旧状态,可能导致数据的不一致。
在执行程序时,为了提高性能,编译器和处理器常常会对指令重排序,但是指令重排序只能保证单线程的语义一致性,不能保证多线程下的语义一致性。多线程共享引入了复杂的数据依赖性,不管编译器和处理器如何对指令重排序,都必须遵从数据的依赖性要求

指令重排序需要满足的条件

happens-before 的八大原则

如果两个操作不满足上述任意一个 happens-before 规则,那么这两个操作就没有顺序的保障,JVM 可以对这两个操作进行重排序。
如果操作 A happens-before 操作 B,那么操作 A 在内存上所做的操作对 B 是可见的
happens-before 原则非常重要,它是判断线程是否安全、数据是否存在竞争的主要依据。
下面是一个分析例子:

private int value=0;
//线程 A 执行该方法
public void write(int input){
    value=input;
}

//线程 B 执行该方法
public int read(){
    return value;
}

线程 A 执行 write() 方法给 value 赋值,线程 B 执行 read() 方法读取 value 的值。
但是这段代码不满足 happens-before 的八大原则,无法保证线程 A 执行的结果对线程 B 是可见的。我们可以通过两个办法解决这个线程安全问题。

happens-before 的实现是依赖于内存屏障,通过禁止某些指令重排序保证内存可见性。

volatile 在并发编程中很常见,下面来谈谈 volatile 的内存语义是如何实现共享变量在多线程中的可见性的。

volatile 是 JVM 提供的轻量级同步机制,由如下两个作用:

虽然对 volatile 变量的写操作总是能立即反映到其他线程中,但是如果对 volatile 变量的运算操作不是原子性的,那么在多线程环境中不能保证安全性,下面是一个例子:

public class VolatileVisibility {
    public static volatile int value=0;
    public static void increase(){
        value++;
    }
}

在上面的代码中,value 变量被 volatile 修饰,对 value 变量的改变会立刻反映到其他线程中。但是如果多条线程同时调用 increase() 方法时,还是会出现线程安全问题,因为value++这个操作并不具备原子性。
我们可以使用javap指令来查看上面increase()方法的字节码,如下:

  public static void increase();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #2                  // Field value:I
         3: iconst_1
         4: iadd
         5: putstatic     #2                  // Field value:I
         8: return

可以看到value++在字节码层面是如下步骤:

<div align="center"> <img src="https://user-gold-cdn.xitu.io/2020/4/7/171554d42affac38?w=857&h=429&f=png&s=4626"/> </div>

上述的操作不是原子性的,如果两个线程同时执行increase()方法,预料中的结果应该是value+2。一个线程把value读入到了自己的操作数栈中,但是还没执行 +1 操作,此时另一个线程也读取了value到自己的操作数栈中进行 +1 操作,最终两个线程返回的结果都是value+1,引发了线程安全的问题。因此必须使用synchronized修饰increase()方法保证线程安全,使得先获得锁的线程的操作happens-before于随后获得这个锁的线程的操作。而且由于synchronized也可以保证操作的可见性,这时可以不用volatile修饰value变量。

volatile 的可见性

如果对volatile变量的运算操作是原子性的。那么就可以保证该变量的线程安全,下面是一个例子

public class VolatileSafe {
    private volatile boolean shutDown;
    public void close() {
        shutDown=true;
    }

    public void doWork(){
        while (!shutDown){
            System.out.println("safe...");
        }
    }
}

在这个例子中,对boolean变量的修改是原子性的,因此对这个变量的修改对其他线程立即可见,保证了线程安全。

对 volatile 变量的修改为什么可以做到立即可见?

volatile 是通过内存屏障来禁止指令重排序优化的。
内存屏障的作用有以下两点:

下面来分析一个带有隐患的常见的单例写法:

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        //第一次检测
        if (instance == null) {
            //同步
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

一个对象的初始化不是原子性的操作,可以分为 3 步:

上述流程可能经过重排序。变为如下顺序:

我们假设线程 A 先执行 getInstance() 方法,当执行完指令 2 时恰好发生了线程切换,切换到了线程 B 上;如果此时线程 B 也执行 getInstance() 方法,那么线程 B 在执行第一个判断时会发现 instance != null,所以直接返回 instance,而此时的 instance 是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常。
解决方法是使用volatile修饰instance变量,禁止指令重排序即可。

volatilesynchronized的区别

如果你觉得这篇文章对你有帮助,不妨点个赞,让我有更多动力写出好文章。

我的文章会首发在公众号上,欢迎扫码关注我的公众号张贤同学
<div align="center"><img src="https://image.zhangxiann.com/QRcode_8cm.jpg"/></div>

上一篇下一篇

猜你喜欢

热点阅读