写在开头

在之前的几篇博文中,咱们都提到了 volatile 关键字,这个单词中文释义为:不稳定的,易挥发的,在Java中代表变量修饰符,用来修饰会被不同线程访问和修正的变量,对于方法,代码块,方法参数,局部变量以及实例常量,类常量多不能进行修饰。

自JDK1.5之后,官网对volatile进行了语义增强,这让它在Java多线程范畴越发重要!因而,咱们今日就抽一晚上时间,来学一学这个关键字,首先,咱们从标题下手,考虑这样的一个问题:

volatile是怎样确保可见性的?又是怎样制止指令重排的,它为什么不能完成原子性呢?

带着疑问,咱们一同走进volatile的国际,探究它与可见性,有序性,原子性之间的爱恨情仇!

volatile怎样确保可见性?

volatile确保了不同线程对同享变量进行操作时的可见性,即一个线程修正了同享变量的值,同享变量修正后的值对其他线程立即可见。

咱们先经过之前写的一个小案例来感受一下什么是可见性问题:

【代码示例1】

public class Test {
    //是否中止 变量
    private static boolean stop = false;
    public static void main(String[] args) throws InterruptedException {
        //发动线程 1,当 stop 为 true,完毕循环
        new Thread(() -> {
            System.out.println("线程 1 正在运转...");
            while (!stop) ;
            System.out.println("线程 1 停止");
        }).start();
        //休眠 1 秒
        Thread.sleep(1000);
        //发动线程 2, 设置 stop = true
        new Thread(() -> {
            System.out.println("线程 2 正在运转...");
            stop = true;
            System.out.println("设置 stop 变量为 true.");
        }).start();
    }
}

输出:

线程 1 正在运转...
线程 2 正在运转...
设置 stop 变量为 true.

原因: 咱们会发现,线程1运转起来后,休眠1秒,发动线程2,可即使线程2把stop设置为true了,线程1依然没有中止,这个便是由于 CPU 缓存导致的可见性导致的问题。线程 2 设置 stop 变量为 true,线程 1 在 CPU 1上履行,读取的 CPU 1 缓存中的 stop 变量依然为 false,线程 1 一直在循环履行。

走进volatile的国际,探究它与可见性,有序性,原子性之间的爱恨情仇!
那这个问题怎样处理呢?很好处理!咱们排volatile上场能够秒搞定,只需求给stop变量加上volatile修饰符即可!

【代码示例2】

//给stop变量添加volatile修饰符
private static volatile boolean stop = false;

输出:

线程 1 正在运转...
线程 2 正在运转...
设置 stop 变量为 true.
线程 1 停止

从成果中看,线程1成功的读取到了线程而设置为true的stop变量值,处理了可见性问题。那volatile究竟是什么让变量在多个线程之间坚持可见性的呢?请看下图!

走进volatile的国际,探究它与可见性,有序性,原子性之间的爱恨情仇!
假如咱们将变量声明为 volatile ,这就指示 JVM,这个变量是同享且不稳定的,每次使用它都到主存中进行读取,详细完成可总结为5步。

  • 1️⃣在生成最低成汇编指令时,对volatile修饰的同享变量写操作添加Lock前缀指令,Lock 前缀的指令会引起 CPU 缓存写回内存;
  • 2️⃣CPU 的缓存回写到内存会导致其他 CPU 缓存了该内存地址的数据无效;
  • 3️⃣volatile 变量经过缓存共同性协议确保每个线程取得最新值;
  • 4️⃣缓存共同性协议确保每个 CPU 经过嗅探在总线上传达的数据来检查自己缓存的值是不是修正;
  • 5️⃣当 CPU 发现自己缓存行对应的内存地址被修正,会将当前 CPU 的缓存行设置成无效状况,重新从内存中把数据读到 CPU 缓存。

volatile怎样确保有序性?

在之前的学习咱们了解到,为了充分利用缓存,提高程序的履行速度,编译器在底层履行的时分,会进行指令重排序的优化操作,但这种优化,在有些时分会带来 有序性 的问题。

那何为有序性呢?咱们能够浅显理解为:程序履行的次序要依照代码的先后次序。 当然,之前咱们还说过发生有序性问题时,咱们能够经过给变量添加volatile修饰符进行处理。

首先,咱们来回忆一下之前写的一个关于有序性问题的测试类。 【代码示例1】

int a = 1;(1)
int b = 2;(2)
int c = a + b;(3)

上面的这段代码中,c变量依赖a,b的值,因而,在编译器优化重排时,c必定会在a,b赋值以后履行,但a,b之间没有依赖关系,可能会发生重排序,但这种重排序即使到了多线程中仍旧不会存在问题,由于即使重排对履行成果也无影响。

但有些时分,指令重排序能够确保串行语义共同,但是没有责任确保多线程间的语义也共同,咱们持续看下面这段代码:

【代码示例2】

public class Test {
    private static int num = 0;
    private static boolean ready = false;
    //制止指令重排,处理次序性问题
    //private static volatile boolean ready = false;
    public static class ReadThread extends Thread {
        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                if (ready) {//(1)
                    System.out.println(num + num);//(2)
                }
                System.out.println("读取线程...");
            }
        }
    }
    public static class WriteRead extends Thread {
        @Override
        public void run() {
            num = 2;//(3)
            ready = true;//(4)
            System.out.println("赋值线程...");
        }
    }
    public static void main(String[] args) throws InterruptedException {
        ReadThread rt = new ReadThread();
        rt.start();
        WriteRead wr = new WriteRead();
        wr.start();
        Thread.sleep(10);
        rt.interrupt();
        System.out.println("rt stop...");
    }
}

咱们界说了2个线程,一个用来求和操作,一个用来赋值操作,由于界说的是成员变量,所以代码(1)(2)(3)(4)之间不存在依赖关系,在运转时极可能发生指令重排序,如将(4)在(3)前履行,次序为(4)(1)(3)(2),这时输出的便是0而不是4,但在很多功能比较好的电脑上,这种重排序状况不易复现。 这时,咱们给ready 变量添加一个volatile关键字,就成功的处理问题了。

volatile关键字能够制止指令重排的原因主要有两个!

一、3 个 happens-before 规矩的完成

  1. 对一个 volatile 变量的写 happens-before 恣意后续对这个 volatile 变量的读;
  2. 一个线程内,依照程序代码次序,书写在前面的操作先行发生于书写在后边的操作;
  3. happens-before 传递性,A happens-before B,B happens-before C,则 A happens-before C。

二、内存屏障 变量声明为 volatile 后,在对这个变量进行读写操作的时分,会经过刺进特定的 内存屏障 的方法来制止指令重排序。

内存屏障(Memory Barrier 又称内存栅门,是一个 CPU 指令),为了完成volatile 内存语义,volatile 变量的写操作,在变量的前面和后边分别刺进内存屏障;volatile 变量的读操作是在后边刺进两个内存屏障。

详细屏障规矩:

  1. 在每个 volatile 写操作的前面刺进一个 StoreStore 屏障;
  2. 在每个 volatile 写操作的后边刺进一个 StoreLoad 屏障;
  3. 在每个 volatile 读操作的后边刺进一个 LoadLoad 屏障;
  4. 在每个 volatile 读操作的后边刺进一个 LoadStore 屏障。

屏障阐明:

  1. StoreStore:制止之前的一般写和之后的 volatile 写重排序;
  2. StoreLoad:制止之前的 volatile 写与之后的 volatile 读/写重排序;
  3. LoadLoad:制止之后一切的一般读操作和之前的 volatile 读重排序;
  4. LoadStore:制止之后一切的一般写操作和之前的 volatile 读重排序。

OK,知道了这些内容之后,咱们再回头看代码示例2中,添加了volatile关键字后的履行次序,在赋值线程发动后,履行次序会变成(3)(4)(1)(2),这时打印的成果就为4啦!

volatile为什么不能确保原子性?

咱们讲完了volatile修饰符确保可见性与有序性的内容,接下来咱们考虑另外一个问题,它能够确保原子性吗?为什么?咱们仍旧经过一段代码去证明一下!

【代码示例3】

public class Test {
    //计数变量
    static volatile int count = 0;
    public static void main(String[] args) throws InterruptedException {
        //线程 1 给 count 加 10000
        Thread t1 = new Thread(() -> {
            for (int j = 0; j <10000; j++) {
                count++;
            }
            System.out.println("thread t1 count 加 10000 完毕");
        });
        //线程 2 给 count 加 10000
        Thread t2 = new Thread(() -> {
            for (int j = 0; j <10000; j++) {
                count++;
            }
            System.out.println("thread t2 count 加 10000 完毕");
        });
        //发动线程 1
        t1.start();
        //发动线程 2
        t2.start();
        //等候线程 1 履行完成
        t1.join();
        //等候线程 2 履行完成
        t2.join();
        //打印 count 变量
        System.out.println(count);
    }
}

咱们创建了2个线程,分别对count进行加10000操作,理论上最终输出的成果应该是20000万对吧,但实践并不是,咱们看一下真实输出。

输出:

thread t1 count 加 10000 完毕
thread t2 count 加 10000 完毕
14281

原因: Java 代码中 的 count++并非原子的,而是一个复合性操作,至少需求三条CPU指令:

  • 指令 1:把变量 count 从内存加载到CPU的寄存器
  • 指令 2:在寄存器中履行 count + 1 操作
  • 指令 3:+1 后的成果写入CPU缓存或内存

即使是单核的 CPU,当线程 1 履行到指令 1 时发生线程切换,线程 2 从内存中读取 count 变量,此时线程 1 和线程 2 中的 count 变量值是相等,都履行完指令 2 和指令 3,写入的 count 的值是相同的。从成果上看,两个线程都进行了 count++,但是 count 的值只添加了 1。这种状况多发生在cpu占用时间较长的线程中,若单线程对count仅添加100,那咱们就很难遇到线程的切换,得出的成果也便是200啦。

要想处理也很简单,利用 synchronized、Lock或者AtomicInteger都能够,咱们在后边的文章中会聊到的,请持续坚持关注哦!

结束彩蛋

假如本篇博客对您有必定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥!

走进volatile的国际,探究它与可见性,有序性,原子性之间的爱恨情仇!

假如您想与Build哥的关系更近一步,还能够关注“JavaBuild888”,在这里除了看到《Java成长方案》系列博文,还有提高工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!

走进volatile的国际,探究它与可见性,有序性,原子性之间的爱恨情仇!