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

Java 多线程与并发——JMM 的内存可见性
23/100
发布文章
smartbetter
未选择任何文件
new
JMM(Java 内存模型)是一种抽象的概念,并不真实存在,它描述的是一组围绕原子性、有序性、可见性的规范。通过这组规范定义了程序中各个变量的访问方式,包括实例字段、静态字段和构成数组对象的元素。

Java 内存模型

如上图,JMM 中的主内存存储 Java 实例对象,包括成员变量、类信息、常量、静态变量等,属于数据共享的区域,多线程并发操作会引发线程安全问题。JMM 中的工作内存存储当前方法的所有本地变量信息,本地变量对其他线程不可见,还存储了字节码行号指示器、Native 方法信息,属于线程私有数据区域,不存在线程安全问题。

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

在执行程序的时候,为了提高性能,CPU 和编译器常常会对指令进行重排序,指令重排序需要满足在单线程环境下不能改变程序运行的结果且不存在数据依赖关系。也就是无法通过 happens-before 原则推导出来的,才能进行指令重排序。

JVM 内部的实现通常依赖于所谓的内存屏障,通过禁止某些重排序的方式提供内存可见性保证。A 操作的结果需要对 B 操作可见,则 A 与 B 必须存在 happens-before 关系。happens-before 原则非常重要,它是判断数据是否存在竞争,线程是否安全的主要依据。

volatile 关键词是 JVM 提供的轻量级同步机制。

volatile 的本质是告诉 JVM 当前变量在工作内存中的值是不确定的,需要从主存中读取。当写一个 volatile 变量时,JMM 会把该线程对应工作内存中的共享变量值刷新到主内存中;当读取一个 volatile 变量时,JMM 会把该线程对应工作内存中的共享变量值置为无效,从而从主存中读取。从而保证了被 volatile 修饰的共享变量对所有线程总是可见的。

volatile 关键词会禁止指令重排序优化。

private static volatile int value = 0;

使用 volatile 关键词实现了实例变量在多个线程之间的可见性。但 volatile 关键词最致命的缺点是不支持原子性。例如以下代码:

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

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

所以对于 increase() 方法可以使用 synchronized 修饰,value++; 的操作变成了原子性操作,保证了线程安全:

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 的同步机制都是围绕这两个方面来确保线程安全的。

常见的问题:

1、volatile 变量为何立即可见?

  • 当写一个 volatile 变量时,JMM 会把该线程对应的工作内存中的共享变量值刷新到主内存中;
  • 当读取一个 volatile 变量时,JMM 会把该线程对应的工作内存置为无效。

2、volatile 如何禁止重排优化?

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

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

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

3、多线程下,volatile 修饰的变量可以计数吗?

volatile 修饰的变量不可以计数,虽然使用 volatile 关键词实现了实例变量在多个线程之间的可见性,但 volatile 关键词最致命的缺点是不支持原子性。例如以下代码:

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

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

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

Java 内存模型

如上图,JMM 中的主内存存储 Java 实例对象,包括成员变量、类信息、常量、静态变量等,属于数据共享的区域,多线程并发操作会引发线程安全问题。JMM 中的工作内存存储当前方法的所有本地变量信息,本地变量对其他线程不可见,还存储了字节码行号指示器、Native 方法信息,属于线程私有数据区域,不存在线程安全问题。

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

在执行程序的时候,为了提高性能,CPU 和编译器常常会对指令进行重排序,指令重排序需要满足在单线程环境下不能改变程序运行的结果且不存在数据依赖关系。也就是无法通过 happens-before 原则推导出来的,才能进行指令重排序。

JVM 内部的实现通常依赖于所谓的内存屏障,通过禁止某些重排序的方式提供内存可见性保证。A 操作的结果需要对 B 操作可见,则 A 与 B 必须存在 happens-before 关系。happens-before 原则非常重要,它是判断数据是否存在竞争,线程是否安全的主要依据。

volatile 关键词是 JVM 提供的轻量级同步机制。

volatile 的本质是告诉 JVM 当前变量在工作内存中的值是不确定的,需要从主存中读取。当写一个 volatile 变量时,JMM 会把该线程对应工作内存中的共享变量值刷新到主内存中;当读取一个 volatile 变量时,JMM 会把该线程对应工作内存中的共享变量值置为无效,从而从主存中读取。从而保证了被 volatile 修饰的共享变量对所有线程总是可见的。

volatile 关键词会禁止指令重排序优化。

private static volatile int value = 0;
使用 volatile 关键词实现了实例变量在多个线程之间的可见性。但 volatile 关键词最致命的缺点是不支持原子性。例如以下代码:

public class VolatileVisibility {
private static volatile int value = 0;
public static void increase() {
value++;
}
}
如果现在多条线程同时调用 increase() 方法,就会出现线程安全问题,因为 value++; 这个操作并不具备原子性,value++; 会先从主内存中读取值到工作内存中,再写回主内存一个新值,分两步完成,如果另外一个线程在两步之间从主内存中读取 value 值,那么第二个线程就会与第一个线程一起看到同一个 value 值,并执行相同的 +1 操作,也就引发了线程安全的问题。

所以对于 increase() 方法可以使用 synchronized 修饰,value++; 的操作变成了原子性操作,保证了线程安全:

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 的同步机制都是围绕这两个方面来确保线程安全的。

常见的问题:

1、volatile 变量为何立即可见?

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

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

保证了特点操作的执行顺序;
保证了某些变量的内存可见性。
volatile 通过插入内存屏障指令,禁止在内存屏障前后的指令执行重排序优化。
volatile 强制刷出各种 CPU 的缓存数据,因此任何 CPU 上的线程都能读取到这些数据的最新版本。

3、多线程下,volatile 修饰的变量可以计数吗?

volatile 修饰的变量不可以计数,虽然使用 volatile 关键词实现了实例变量在多个线程之间的可见性,但 volatile 关键词最致命的缺点是不支持原子性。例如以下代码:

public class VolatileVisibility {
private static volatile int value = 0;
public static void increase() {
value++;
}
}
如果现在多条线程同时调用 increase() 方法,就会出现线程安全问题,因为 value++; 这个操作并不具备原子性,value++; 会先从主内存中读取值到工作内存中,再写回主内存一个新值,分两步完成,如果另外一个线程在两步之间从主内存中读取 value 值,那么第二个线程就会与第一个线程一起看到同一个 value 值,并执行相同的 +1 操作,也就引发了线程安全的问题。

新增投票功能

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值