深入浅出谈谈Java并发编程:Volatile

2023年2月7日11:27:11

Volatile关键字是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量, 相比synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。 但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。

一、volatile变量的特性

1.1、保证可见性,不保证原子性

  • 当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去;
  • 这个写会操作会导致其他线程中的volatile变量缓存无效。

来看一段代码:

public class Test {
    public static void main(String[] args) {
        WangZai wangZai = new WangZai();
        wangZai.start();
        for(; ;){
            if(wangZai.isFlag()){
                System.out.println("hello");
            }
        }
    }

    static class WangZai extends Thread {

        private boolean flag = false;

        public boolean isFlag(){
            return flag;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = true;
            System.out.println("flag = " + flag);
        }
    }
}

你会发现,永远都不会输出hello这一段代码,按道理线程改了flag变量,主线程也能访问到的呀?

但是将flag变量用volatile修饰一下,就能输出hello这段代码

private volatile boolean flag = false;

每个线程操作数据的时候会把数据从主内存读取到自己的工作内存,如果他操作了数据并且写会了,那其他已经读取的线程的变量副本就会失效了,需要对数据进行操作又要再次去主内存中读取了。

volatile保证不同线程对共享变量操作的可见性,也就是说一个线程修改了volatile修饰的变量,当修改写回主内存时,另外一个线程立即看到最新的值。

1.2、禁止指令重排

重排序需要遵守一定规则:

  • 重排序操作不会对存在数据依赖关系的操作进行重排序。
  • 重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变。

什么是重排序?

为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序。

重排序的类型有哪些呢?

深入浅出谈谈Java并发编程:Volatile

一个好的内存模型实际上会放松对处理器和编译器规则的束缚,也就是说软件技术和硬件技术都为同一个目标,而进行奋斗:在不改变程序执行结果的前提下,尽可能提高执行效率。

JMM对底层尽量减少约束,使其能够发挥自身优势。

因此,在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序。

一般重排序可以分为如下三种:

  • 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
  • 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
  • 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。

那 Volatile 是怎么保证不会被执行重排序的呢?

二、内存屏障

java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。

为了实现volatile的内存语义,JMM会限制特定类型的编译器和处理器重排序,JMM会针对编译器制定volatile重排序规则表:

是否能重排序第二个操作第一个操作普通读/写volatile读volatile写普通读/写NOvolatile读NONONOvolatile写NONO

举例来说,第三行最后一个单元格的意思是:在程序顺序中,当第一个操作为普通变量的读或写时,如果第二个操作为volatile写,则编译器不能重排序这两个操作。

从上表我们可以看出:

  • 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
  • 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
  • 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

需要注意的是:volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障

深入浅出谈谈Java并发编程:Volatile

深入浅出谈谈Java并发编程:Volatile

从JDK5开始,提出了happens-before的概念,通过这个概念来阐述操作之间的内存可见性。

三、happens-before

happens-before 关系的定义:

  • 如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果就会对第二个操作可见。
  • 两个操作之间如果存在 happens-before 关系,并不意味着 Java 平台的具体实现就必须按照 happens-before 关系指定的顺序来执行。如果重排序之后的执行结果,与按照 happens-before 关系来执行的结果一直,那么 JMM 也允许这样的重排序。

看到这儿,你是不是觉得,这个怎么和 as-if-serial 语义一样呢。没错, happens-before 关系本质上和 as-if-serial 语义是一回事。

as-if-serial 语义保证的是单线程内重排序之后的执行结果和程序代码本身应该出现的结果是一致的,

happens-before 关系保证的是正确同步的多线程程序的执行结果不会被重排序改变。

一句话来总结就是:如果操作 A happens-before 操作 B ,那么操作 A 在内存上所做的操作对操作 B 都是可见的,不管它们在不在一个线程。

在 Java 中,对于 happens-before 关系,有以下规定:

  • 程序顺序规则:一个线程中的每一个操作, happens-before 于该线程中的任意后续操作。
  • 监视器锁规则:对一个锁的解锁, happens-before 于随后对这个锁的加锁。
  • volatile 变量规则:对一个 volatile 域的写, happens-before 与任意后续对这个 volatile 域的读。
  • 传递性:如果 A happens-before B , 且 B happens-before C ,那么 A happens-before C。
  • start 规则:如果线程 A 执行操作 ThreadB。start() 启动线程 B ,那么 A 线程的 ThreadB。start() 操作 happens-before 于线程 B 中的任意操作。
  • join 规则:如果线程 A 执行操作 ThreadB。join() 并成功返回,那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB。join() 操作成功返回。
  • 作者:该用户快成仙了
  • 原文链接:https://blog.csdn.net/weixin_60707895/article/details/121013097
    更新时间:2023年2月7日11:27:11 ,共 2891 字。