• 浅谈 volatile


    1.简介

    volatile 在Java日常开发中,尤其是并发编程中,是一个高频使用的关键字,它提供了一种轻量级的同步机制(常用于和synchronized比较),用来修饰成员变量。volatile 具有如下两大特性:

    1. 保证内存可见性
    2. 禁止指令重排序

    volatile 无法保证原子性

    2.内存可见性

    我们先看一段代码:

    1. public class Test {
    2. public static boolean flag = false;
    3. public static void main(String[] args) throws Exception {
    4. Thread t1 = new Thread(new Runnable() {
    5. @Override
    6. public void run() {
    7. //子线程休眠1s,让主线程启动后再去修改flag
    8. try {
    9. TimeUnit.SECONDS.sleep(1);
    10. } catch (InterruptedException e) {
    11. e.printStackTrace();
    12. }
    13. //TODO: 修改成员变量
    14. flag = true;
    15. System.out.println("子线程修改了flag = " + flag);
    16. }
    17. });
    18. t1.start();
    19. while (true) {
    20. if (flag) {
    21. System.out.println("主线程退出");
    22. break;
    23. }
    24. }
    25. }
    26. }

    从运行结果中可以看到,子线程t1中将flag修改为了true,但是主线程并没有读到t1线程修改后的值,导致主线程一直无法退出。所以多线程情况下修改共享变量会出现某一个线程修改后对其他线程不可见

    在分析为什么不可见之前,我们先了解下Java内存模型JMM(和并发编程相关的模型)

    2.1 JMM

    JMM即为JAVA 内存模型(java memory model)。他是虚拟机规范定义的一种内存模型,因为在不同的硬件生产商和不同的操作系统下,内存的访问逻辑有一定的差异,结果就是当你的代码在某个系统环境下运行良好,并且线程安全,但是换了个系统就出现各种问题。Java内存模型是一种标准化,屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果。JMM从java 5开始的JSR-133发布后,已经成熟和完善起来。

    Java内存模型(java memory model)描述了程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量的底层细节。 JMM 有如下规定

    1. 所有的共享变量都位于主内存,这里所说的共享变量指的是实例变量和类变量;不包括局部变量,因为局部变量是线程私有的,不存在竞争问题。
    2. 每一个线程还有自己的工作内存,线程个工作内存保存了被线程使用的变量的工作副本。
    3. 线程对变量的所有操作(read,write)都必须在工作内存中完成,而不能直接读写主内存中的变量。
    4. 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。

    本地内存和主线程的关系:

    JVM在设计时候考虑到,如果JAVA线程每次读取和写入变量都直接操作主内存,对性能影响比较大,所以每条线程拥有各自的工作内存,工作内存中的变量是主内存中的一份拷贝,线程对变量的读取和写入,直接在工作内存中操作,而不能直接去操作主内存中的变量。但是这样就会出现一个问题,当一个线程修改了自己工作内存中变量,对其他线程是不可见的,会导致线程不安全的问题。因为JMM制定了一套标准来保证开发者在编写多线程程序的时候,能够控制什么时候内存会被同步给其他线程

    基于以上JMM的知识,我们就可以知道为什么我们的代码会出现不可见的问题.

    顺便说一下:工作内存是 JMM 的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

    2.2 不可见问题分析

    1. 子线程t1从主内存够中将共享变量flag=false读到自己的工作内存中
    2. 主线程也将共享变量flag=false读到自己的工作内存中
    3. 子线程t1在工作内存中将flag修改为true,然后写回到主内存中
    4. 虽然主内存中的flag已经从false变为了true, 但是对于主线程来说,它使用仍然是自己工作内存中的flag, 也就是false

    上图中是一个大致的过程

    1. read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;
    2. load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;
    3. use(使用):作用于工作内存,它把工作内存中的变量给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;
    4. assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;
    5. store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;
    6. write(写入):作用于主内存,它把store传送值放到主内存中的变量中。
    7. lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;
    8. unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;

    那么细化后,我们用下面这张图来展示:

    2.3 不可见问题的原因

    所有的共享变量都位于主内存中,每个线程有自己的本地内存(工作内存),而线程的工作内存是私有的,所以线程读写共享数据是必须经过主内存交换才可以,这就是产生不可见的原因。

    根据上图可以知道,t1线程写回到主线程后,main线程无法感知,仍然使用自己工作内存中的变量。

    2.4 解决变量的不可见问题

    在多线程下实现共享变量的可见性,有2种解决方案,一种是加锁(synchronized),另一种就是今天的主角:volatile

    2.4.1 使用加锁解决方案

    直接修改上面的代码:

    1. public class Test {
    2. public static boolean flag = false;
    3. public static void main(String[] args) throws Exception {
    4. Thread t1 = new Thread(new Runnable() {
    5. @Override
    6. public void run() {
    7. //子线程休眠1s
    8. try {
    9. TimeUnit.SECONDS.sleep(1);
    10. } catch (InterruptedException e) {
    11. e.printStackTrace();
    12. }
    13. //TODO: 修改成员变量
    14. flag = true;
    15. System.out.println("子线程修改了flag = " + flag);
    16. }
    17. });
    18. t1.start();
    19. while (true) {
    20. //TODO:加锁
    21. synchronized (t1) {
    22. if (flag) {
    23. System.out.println("主线程退出");
    24. break;
    25. }
    26. }
    27. }
    28. }
    29. }

    当某一个线程进入synchronized代码块前后,它的执行过程简单如下:

    1. 线程获得锁
    2. 清空工作内存
    3. 从主内存拷贝共享变量的最新值到工作内存成为副本
    4. 执行代码
    5. 将修改后的副本值刷回到主内存中
    6. 线程释放锁

    所以,当主线程进入while循环后,清空自己的工作内存,然后从主内存中拷贝flag到自己的工作内存中。注意:synchronized要放到while内部,这样它就会不断的从主内存中读取flag, 直到读取到新值为true。

    2.4.2 使用volatile解决方案

    就是给共享变量flag添加volatile关键字,请看代码:

    1. public class Test {
    2. //TODO:给共享变量添加 volatile
    3. public static volatile boolean flag = false;
    4. public static void main(String[] args) throws Exception {
    5. Thread t1 = new Thread(new Runnable() {
    6. @Override
    7. public void run() {
    8. //子线程休眠1s
    9. try {
    10. TimeUnit.SECONDS.sleep(1);
    11. } catch (InterruptedException e) {
    12. e.printStackTrace();
    13. }
    14. //TODO: 修改成员变量
    15. flag = true;
    16. System.out.println("子线程修改了flag = " + flag);
    17. }
    18. });
    19. t1.start();
    20. while (true) {
    21. if (flag) {
    22. System.out.println("主线程退出");
    23. break;
    24. }
    25. }
    26. }
    27. }

    1. 子线程t1从主内存够中将共享变量flag=false读到自己的工作内存中
    2. 主线程也将共享变量flag=false读到自己的工作内存中
    3. 子线程t1在工作内存中将flag修改为true,然后立即写回到主内存中
    4. 然后底层通过总线机制,让持有该共享变量副本的其他线程(也就是主线程)失效,然后重新从主内存中读取共享变量到工作内存中

    总结:volatile保证了多个线程对共享变量操作的可见性,也就是说某一个线程修改了共享变量并写回主存后,其他线程会立即看到修改后的新值

    如果还想探究某个线程写回主内存后,其他线程如何让自己的工作内存里的变量失效,以及如何感知主内存中的共享变量被修改了等这些细节内容,这就需要了解缓存一致性协议

    3.缓存一致性协议

    在学习缓存一致性协议之前,先首先简单看下计算机的大致结构

    CPU的运算速度最快,内存的读写速度无法和其

  • 相关阅读:
    PDO:从 MySQL 中选择行
    数据结构-二叉树力扣题
    BERT 相关资源整理
    计算机毕业设计(附源码)python智能交通信息管理平台
    ChatGPT发不出消息?GPT发不出消息怎么办?
    BFD双向转发检测协议理论讲解
    迅为RK3568开发板Busybox 制作最小文件系统固定IP测试
    Java本地远程断点调试(实战记录)
    STM32F4X之中断二
    计算机操作系统 第二章:进程 的描述与控制(4)
  • 原文地址:https://blog.csdn.net/jcc4261/article/details/128181007