Java 多线程与并发——JMM 的内存可见性

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

Java 内存模型(JMM):

Java 多线程与并发——JMM 的内存可见性_第1张图片

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

JMM 中的工作内存:存储当前方法的所有本地变量信息,本地变量对其他线程不可见 。还存储了字节码行号指示器、Native 方法信息。属于线程私有数据区域,不存在线程安全问题。

主内存共享的方式是线程各拷贝一份数据到工作内存,操作完成后刷新回主内存。

Java 多线程与并发——JMM 的内存可见性_第2张图片

JMM 与 Java 内存区域(见上图运行时数据区)的划分是不同的概念层次,JMM 描述的是一组规则,围绕原子性、有序性、可见性展开。

在执行程序的时候,为了提高性能,处理器和编译器常常会对指令进行重排序,指令重排序需要满足以下两个条件:

  • 在单线程环境下不能改变程序运行的结果;
  • 存在数据依赖关系的不允许重排序。

也就是无法通过 happens-before 原则推导出来的,才能进行指令的重排序。

JVM 内部的实现通常依赖于所谓的内存屏障,通过禁止某些重排序的方式提供内存可见性保证。A 操作的结果需要对 B 操作可见,则 A 与 B 必须存在 happens-before 关系。

happens-before 原则非常重要,它是判断数据是否存在竞争,线程是否安全的主要依据。

1.volatile关键词

volatile 关键词是 JVM 提供的轻量级同步机制,保证了被 volatile 修饰的共享变量对所有线程总是可见的,并且禁止指令重排序优化。

volatile 关键词强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值。

private static volatile int value = 0;

线程主体(工作内存) <—— volatile 变量写入/读取——> 主内存

使用 volatile 关键词增加了实例变量在多个线程之间的可见性。但 volatile 关键词最致命的缺点是不支持原子性。volatile 关键词虽然增加了实例变量在多个线程之间的可见性,但它却不具备同步性,那么也就不具备原子性。例如以下代码:

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

value 值的任何改变,都会立马反应到线程当中,如果现在存在多条线程同时调用 increase() 方法,就会出现线程安全问题,因为 value++; 这个操作并不具备原子性,value++; 是先读取值,再写回一个新值,分两步完成,如果另外一个线程在两步之间读取 value 值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同的 +1 操作,也就引发了线程安全的问题。

所以对于 increase() 方法必须使用 synchronized 修饰,保证线程安全:

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

由于 synchronized 会创建一个内存屏障,保证所有结果都刷新到主存中去,保证了操作的内存可见性,所以 volatile 也就可以省略了:

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

synchronized 与 volatile 对比:

关键词 性能 修饰范围 多线程访问 原子性可见性 指令重排序 主要解决问题
synchronized 可修饰方法、代码块 会阻塞 可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据同步 可以被重排序优化 解决的是多个线程之间访问资源的同步性
volatile 轻量级实现,性能要好 可修饰变量 不会阻塞 可以保证变量的修改可见性,不能保证原子性 禁止重排序 解决的是变量在多个线程之间的可见性

线程安全包含原子性和可见性两个方面,Java 的同步机制都是围绕这两个方面来确保线程安全的。

2.volatile实现原理

volatile 的本质是告诉 JVM 当前变量在工作内存中的值是不确定的,需要从主存中读取。

  1. volatile 变量为何立即可见?
  • 当写一个 volatile 变量时,JMM 会把该线程对应的工作内存中的共享变量值刷新到主内存中;
  • 当读取一个 volatile 变量时,JMM 会把该线程对应的工作内存置为无效。
  1. volatile 如何禁止重排优化?

内存屏障是一个 CPU 指令,其作用有两个:

  • 保证了特点操作的执行顺序;
  • 保证了某些变量的内存可见性。

volatile 通过插入内存屏障指令,禁止在内存屏障前后的指令执行重排序优化。
volatile 强制刷出各种 CPU 的缓存数据,因此任何 CPU 上的线程都能读取到这些数据的最新版本。

你可能感兴趣的:(多线程与并发)