【JMM】保证线程间的可见性,还只知道volatile?

本文目录

前言

举例

情形1 int->Integer

情形2 System.out.println()

情形3  storeFence()

情形4  Thread.yield()

情形5  LockSupport.unpark()

情形6  增长循环内代码执行时间

总结分析

volatile分析

字节码解释器实现

模版解释器实现

其他情形分析


前言

这是一篇关于介绍线程间可见性的文章,当然我们先介绍下什么是可见性( ̄∇ ̄)/

可见性指的是一个线程对共享变量的修改对其他线程是可见的

总所周知,volatile 关键字可以解决能够及时可见的问题,使得修改过的数据能立刻被看到,那么只有加 volatile 关键字才能达到这种效果吗?还有什么情况下线程之间是可见的呢?

我们举一个老生常谈的,代码如下

【JMM】保证线程间的可见性,还只知道volatile?_第1张图片

package com.aqin.custom.aqs;

/**
 * @Description
 * @Author aqin1012 AQin.
 * @Date 5/10/23 3:31 PM
 * @Version 1.0
 */
public class TestVisibility {
   private boolean flag = true;
   private int count = 0;

   public static void main(String[] args) throws InterruptedException {
      TestVisibility testVisibility = new TestVisibility();
      Thread threadA = new Thread(() -> testVisibility.methodA());
      threadA.start();
      Thread.sleep(1000);
      Thread threadB = new Thread(() -> testVisibility.methodB());
      threadB.start();
   }

   public void methodA() {
      System.out.println(Thread.currentThread().getName() + "开始循环♻️ ( ̄∇ ̄)/ ……");
      while (flag) {
         count++;
      }
      System.out.println(Thread.currentThread().getName() + "结束循环♻️ \\(^ω^) ! count=" + count);
   }

   public void methodB() {
      flag = false;
      System.out.println(Thread.currentThread().getName() + "修改 flag: " + flag);
   }
}

上述代码执行时,可以看到,卡住了。。。

我等了很久很久很久。。。还是没跳出循环(见下图)

【JMM】保证线程间的可见性,还只知道volatile?_第2张图片

那么在什么情况下上面的methodA方法会跳出循环,输出System.out.println(Thread.currentThread().getName() + "结束循环♻️ \\(^ω^) ! count=" + count)呢?

给变量count加个volatile关键字修饰下,嘿嘿(如下图),跳出循环了

【JMM】保证线程间的可见性,还只知道volatile?_第3张图片

 关于实现多线程之间的可见性,只有volatile吗?

我们来看看其他会使线程之间可见的情形(。・ω・。)ノ

举例

情形1 int->Integer

当循环内的变量count的类型从int变为Integer

【JMM】保证线程间的可见性,还只知道volatile?_第4张图片

 没错,跳出循环(原因最后统一分析)

情形2 System.out.println()

在循环内部添加一行System.out.println()的输出,可以看到,也会结束循环

【JMM】保证线程间的可见性,还只知道volatile?_第5张图片

情形3  storeFence()

添加内存屏障

【JMM】保证线程间的可见性,还只知道volatile?_第6张图片

情形4  Thread.yield()

使用Thread.yield()方法,让出CPU时间片

【JMM】保证线程间的可见性,还只知道volatile?_第7张图片

情形5  LockSupport.unpark()

使用LockSupportunpark方法

【JMM】保证线程间的可见性,还只知道volatile?_第8张图片

情形6  增长循环内代码执行时间

添加如下方法

public static void stopAfter(long interval) {
    long start = System.nanoTime();  //纳秒
    long end;
    do {
        end = System.nanoTime();
    } while (start + interval >= end);
}

设置一个较长的停顿时间

【JMM】保证线程间的可见性,还只知道volatile?_第9张图片

 减小停顿时间

来~我们开始分析,我们先从较为熟悉的volatile开始吧

总结分析

volatile分析

volatile保证线程之间的可见性依靠的是内存屏障

我们先介绍下volatile在hotspot虚拟机中的实现

字节码解释器实现

字节码解释器 Byte Code Interpreter,用C++实现了JVM 指令(每个JVM指令,比如volatile,一般都是由一个C++的函数实现的),优点是简单容易理解,缺点是执行慢

在其中字节码解释器实现的volatile方法中,会先对字段类型判断,然后执行下面这行代码

OrderAccess::storeload()

这是其实就是内存屏障,接下来我们简要的从操作系统层面分析下字节码解释器中的这个实现volatile功能的storeload()

在Linux系统x86架构下,storeload()方法会先对处理器进行判断,看是否是多核处理器(因为单核不存在可见性的问题),如果是就会使用lock(汇编指令)来实现volatile的功能(类似内存屏障的功能)

lock前缀指令的作用

  1. 确保后续指令执行的原子性操作

  2. 类似内存屏障的功能(lock前缀指令不是内存屏障的指令)

  3. 确保其他副本失效

模版解释器实现

模版解释器 Template Interpreter 中其实对每一个常用的指令都写了一段汇编代码,启动时将每个指令与对应的汇编代码入口绑定,以提升效率

volatile_barrier(Assembler::Membar_mask_bits(Assembler::StoreLoad | Assembler::StoreStore))

关于StoreLoadStoreStore的理解可以参考下表

屏障类型

指令示例

说明

LoadLoad

Load1;LoadLoad;Load2

保证Load1的读取操作在Load2以及后续读取操作之前执行

StoreStore

Store1;StoreStore;Store2

保证在Store2及其后续写操作执行前,Store1的写操作已经刷新到主内存

LoadStore

Load1;LoadStore;Store2

保证在Store2及其后续写操作执行前,Load1的读操作已经结束

StoreLoad

Store1;StoreLoad;Load2

保证在Load2及其后续的读操作执行前,Store1写操作已经刷新到主内存

不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了底层硬件平台的差异,由JVM来为不同的平台生长相应的机器码,Java中提供了四类内存屏障(上表中的)

内存屏障又称内存栅栏,是一个CPU指令,主要有2个功能:

  • 保证特定操作的执行顺序

  • 保证特定变量的内存可见性(利用该特性实现volatile的内存可见性)

其他情形分析

情形1 中的int->Integer后,会跳出循环的原因是Integer中的intfinal修饰

【JMM】保证线程间的可见性,还只知道volatile?_第10张图片

 在Java虚拟机中,对于使用final关键字修饰的变量或对象引用,在写入操作时会生成相应的内存屏障指令,以确保该变量或对象引用的值对其他线程可见(会在写入操作之后插入一个StoreStore屏障和一个StoreLoad屏障),所以其底层还是通过内存屏障来保证线程之间的可见性的。

情形2 中的System.out.println()println()方法加了synchronized关键字

【JMM】保证线程间的可见性,还只知道volatile?_第11张图片

 在使用synchronized关键字时,JVM会插入不同类型的内存屏障来保证临界区内代码的顺序执行以及变量的可见性和原子性,所以其底层还是通过内存屏障来保证线程之间的可见性的。

情形3 storeFence()就是直接手动添加内存屏障了。

情形4 Thread.yield()是使用Thread.yield()方法让出CPU时间片,即通过上下文切换(需要保存上下文)保证了不同线程之间的可见性。在Java中,从一个线程转到另一个线程,当一个线程被切换出去时,它的本地内存中的数据会被刷新到主内存中,而当另一个线程被切换进来时,它的本地内存会从主内存中加载最新的数据,这个过程保证了不同线程之间的可见性。

需要注意的是,上下文切换虽然可以保证线程之间的可见性,但也会带来一定的开销,因为上下文切换一般需要5-10ms,每次切换都需要将本地内存中的数据刷新到主内存中,这会增加系统的负担。因此,在编写多线程程序时,应该尽量避免过多的上下文切换,以提高程序的性能。

情形5 使用LockSupportunpark方法

【JMM】保证线程间的可见性,还只知道volatile?_第12张图片

【JMM】保证线程间的可见性,还只知道volatile?_第13张图片

【JMM】保证线程间的可见性,还只知道volatile?_第14张图片

 可以看到LockSupport.unpark()底层是通过调用UnSafe类实现的,与情形3⃣️ 不同的是,调用的是unpark()方法,而非storeFence()方法。

情形6 增长循环内代码执行时间,使得缓存过期,再次读取时就会重新去主内存中读取数据,即通过缓存淘汰,保证不同线程之间的可见性。

可以看到其实现不同线程之间的可见性方法有很多,无论是通过内存屏障还是通过上下文切换,其底层原理就是需要满足两点

  • 对线程本地变量的修改可以立刻刷新回主内存

  • 同时使得其他线程中该变量的缓存失效

你可能感兴趣的:(Java,java,可见性,JMM,volatile)