写在开头

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

那何为有序性呢?我们可以通俗理解为:程序执行的顺序要按照代码的先后顺序。 当然,之前我们还说过发生有序性问题时,我们可以通过给变量添加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哥!

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

关于volatile与指令重排序的探讨的更多相关文章

  1. Java的多线程机制系列:不得不提的volatile及指令重排序(happen-before)

    一.不得不提的volatile volatile是个很老的关键字,几乎伴随着JDK的诞生而诞生,我们都知道这个关键字,但又不太清楚什么时候会使用它:我们在JDK及开源框架中随处可见这个关键字,但并发专 ...

  2. Java的多线程机制系列:(四)不得不提的volatile及指令重排序(happen-before)

    一.不得不提的volatile volatile是个很老的关键字,几乎伴随着JDK的诞生而诞生,我们都知道这个关键字,但又不太清楚什么时候会使用它:我们在JDK及开源框架中随处可见这个关键字,但并发专 ...

  3. 不得不提的volatile及指令重排序(happen-before)

    微信公众号[程序员江湖] 作者黄小斜,斜杠青年,某985硕士,阿里 Java 研发工程师,于 2018 年秋招拿到 BAT 头条.网易.滴滴等 8 个大厂 offer,目前致力于分享这几年的学习经验. ...

  4. 单例模式+volatile禁止指令重排序

    单例模式: 单例,顾名思义就是只能有一个.不能再出现第二个.就如同地球上没有两片一模一样的树叶一样. 在这里就是说:一个类只能有一个实例,并且整个项目系统都能访问该实例. 单例模式共分为两大类: 懒汉 ...

  5. volatile和指令重排序

    volatile 的作用 1 精致指令重排序 2 多线程访问同一个变量的时候,每次都是取最新的,而不会使用当前cpu缓存的那一份.

  6. Volatile禁止指令重排序(三)

    Volatile禁止指令重排 计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令重排,一般分为以下三种: 源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系 ...

  7. 使用 volatile 关键字保证变量可见性和禁止指令重排序

    volatile 概述 volatile 是 Java 提供的一种轻量级的同步机制.相比于传统的 synchronize,虽然 volatile 能实现的同步性要差一些,但开销更低,因为它不会引起频繁 ...

  8. JVM并发机制的探讨——内存模型、内存可见性和指令重排序

    并发本来就是个有意思的问题,尤其是现在又流行这么一句话:“高帅富加机器,穷矮搓搞优化”. 从这句话可以看到,无论是高帅富还是穷矮搓都需要深入理解并发编程,高帅富加多了机器,需要协调多台机器或者多个CP ...

  9. 关于volatile的可见性和禁止指令重排序的疑惑

    在学习volatile语义的可见性和禁止指令重排序的相关测试中,发现并不能体现出禁止指令重排序的特性 实验代码如下 package com.aaron.beginner.multithread.vol ...

  10. 指令重排序所带来的问题及使用volatile关键字解决问题

    首先看下如下代码: 指令重排序和优化后代码如下:if(!stop)while(true){}volatile最适合使用的是一个线程写.其他线程读的场合,如果有多个线程并发写操作,仍然需要使用锁或者线程 ...

随机推荐

  1. clickhouse导入和导出

    一.连接clickhouse--客户端连接default库clickhouse-client -h localhost --port 9001 -u default --password 123456 ...

  2. SP21690 POWERUP - Power the Power Up 题解

    题目传送门 前置知识 扩展欧拉定理 解法 直接对 \(a\) 和 \(b^c\) 分讨,跑一遍扩展欧拉定理就行了. 另外由于本题的特殊规定 \(0^0=1\),故需要在当 \(a=0\) 时,对 \( ...

  3. 轻松玩转makefile | 变量与模式

    前言 本文通过简单的几个示例,以及对同一个Makefile进行几个版本的迭代,帮助快速的理解变量和模式规则的使用. 1.回顾 在上一篇文章中,我们使用Makefile编译fun.c和main.c这两个 ...

  4. lsattr命令

    lsattr命令 lsattr命令用于显示文件的属性. 语法 lsattr [-RVadlv] [file | folder] 参数 -a: 显示所有文件和目录,包括以.为名称开头字符的额外内建,即现 ...

  5. Linux实现指定用户sftp传输,静止ssh登录

    1.环境 名称 ip server 192.168.1.1 client 192.168.1.2 2.服务器创建repl用户 useradd -m -d /home/repl -s /usr/sbin ...

  6. [攻防世界][Web]ics-06

    打开靶机对应的url,展开是一个网站的样子,其实啥也么有 所有tab都点一遍,发现只有报表中心有内容,url为 http://61.147.171.105:49797/index.php?id=1 猜 ...

  7. 项目实战:Qt管道焊接参数条码打印系统(条码打印机TSC 244 Pro、打印条码、打印中文、打印字符、多张连续打印)

    需求   电脑端通过条码打印机TSC-TTP244 Pro:  1. 打印出尺寸为60*30cm  2. 条码打印机TSC-TTP244 Pro 不干胶纸 (不需要碳带,热敏纸)  3. window ...

  8. 擅长使用iter

    def populate_ranks(votes, ranks): names = list(votes.keys()) names.sort(key=votes.get, reverse=True) ...

  9. 【Azure 应用服务】App Servie网站报403 ModSecurity Action错误

    问题描述 App Service 部署应用程序,然后通过App Gateway(WAF) 提供公网访问,但是一直遇见403报错,刷新页面,回退,重新Web页面能缓解403问题. 问题分析 通过浏览器F ...

  10. 【Azure Redis 缓存】Redisson 连接 Azure Redis出现间歇性 java.net.UnknownHostException 异常

    问题描述 在Java项目中,使用Redisson作为连接Redis的客户端,间歇性的出现了DNS Monitor throwable 错误. DNSMonitor throwable="ja ...