1.关于volatile

volatile是Java语言中的关键字,用来修饰会被多线程访问的共享变量,是JVM提供的轻量级的同步机制,相比同步代码块或者重入锁有更好的性能。它主要有两重语义,一是保证多个线程对共享变量访问的可见性,二防止指令重排序。

2.语义一:内存可见性

2.1 一个例子

public class TestVolatile {public static void main(String[] args) throws InterruptedException {ThreadDemo threadDemo = new ThreadDemo();new Thread(threadDemo).start();threadDemo.flag = false;System.out.println("已将flag置为" + threadDemo.flag);}static class ThreadDemo implements Runnable {boolean flag = true;@Overridepublic void run() {System.out.println("Flag=" + flag);}}
}

当你多次执行代码时,有一定几率会出现这种结果:

已将flag置为false
Flag=trueProcess finished with exit code 0

在主线程将子线程实例的flag置为false后,子线程中的flag竟然还是true。这是怎么回事?这就是多线程的内存可见性问题。对于一个没有volatile修饰的的共享变量,当一个线程对其进行了修改,另一线程并不一定能马上看见这个被修改后的值。为什么会出现这种情况呢?这就要从java的内存模型谈起。

2.2 Java的内存模型(JMM)

详细可参见这篇文章:https://blog.csdn.net/nlznlz/article/details/79998985。

Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。

看看JMM模型会给我们在多线程环境下的读写带来什么样的问题。

  • 当一个线程(线程1)对共享变量进行修改时,修改的并不是主内存中的变量,而是该线程对应的工作内存中该变量的一个副本。
  • 当主内存中的变量值已经被修改,另一个线程(线程2)读取的却还是自己工作内存中的旧值。

这时就出现了共享变量在多线程环境下的可见性问题。如果把线程的工作内存当作主内存的缓存,这个问题的本质就在于如何解决缓存失效问题。那么JMM中是如何解决可见性问题的?这就不得不提到happens-before规则。

2.3 happens-before规则

happens-before规则又叫先行发生规则。它定义了Java内存模型中两项操作的偏序关系,更确切的说,它定义了操作可见性之间的偏序关系。比如A操作 happens-before B操作,并不意味这A操作一定在B操作之前,而是A操作的影响能被操作B观察到,这个影响包括改变了内存中共享变量的值,发送消息等。那么JMM定义了哪些happens-before规则?

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  • 监视器锁规则:对于一个锁的解锁,happens-before于随后对这个锁的加锁。
  • volatile变量规则:对于一个volatile 变量的写,happens-before于任意后续对这个volatile变量的读。

这里对于我们而言重要的是第三点。即对于一个volatile变量,写操作happens-before于读操作,也就是说,一个线程对volatile变量做了修改,另一个线程能马上读到这个被修改后的值。
这样就能解决共享变量在多线程环境下的可见性问题了。结合JMM模型,我们可以继续探讨下volatile是如何做到这点的。

2.4 volatile解决内存可见性问题的原理

当一个变量被修饰为volatile后,对其的读写就会显得比较特别。

  • 写一个volatile变量时,JMM首先修改工作内存中的变量值,并刷新到主内存中。

  • 读一个变量时,JMM会把该线程对应的本地内存置为无效,并从主内存中读取共享变量。

对volatile变量的读写,可以说都是直接对主内存进行的操作,这样虽然会牺牲一些性能,但是解决了“缓存一致性问题”,使得变量在多线程间的可见性得到了很好的保证。

3. 语义二:禁止指令重排

3.1 为什么会有指令重排

为了优化程序性能,编译器和处理器会对Java编译后的字节码和机器指令进行重排序,通俗的说代码的执行顺序和我们在程序中定义的顺序会有些不同,只要不改变单线程环境下的执行结果就行。zejian的这篇文章分析了指令重排给CPU指令流水所带来的性能提升:https://blog.csdn.net/javazejian/article/details/72772461#%E5%A4%84%E7%90%86%E5%99%A8%E6%8C%87%E4%BB%A4%E9%87%8D%E6%8E%92。

但是在多线程环境下,指令重排却可能出现并发问题。比如经典的双重检查锁实现单例模式的例子:

3.2 线程不安全的双重检查单例模式

public class Singleton {private static Singleton instance;private Singleton(){}public static Singleton getInstance(){if(null == instance){synchronized (Singleton.class){if(null == instance){instance = new Singleton();}}}return instance;}
}

运行这段代码我们可能会得到一个匪夷所思的结果:我们获得的单例对象是未初始化的。为什么会出现这种情况?因为指令重排。首先要明确一点,同步代码块中的代码也是能够被指令重排的。然后来看问题的关键:

 instance = new Singleton();

虽然在代码中只有一行,编译出的字节码指令可以用如下三行表示:

  • 为对象分配内存空间;
  • 初始化对象;
  • 将instance变量指向刚分配的内存地址。

由于步骤2、3交换不会改变单线程环境下的执行结果,故而这种重排序是被允许的。也就是我们在初始化对象之前就把instance变量指向了该对象。而如果这时另一个线程刚好执行到代码所示的2处:

if (instance == null)

那么这时候有意思的事情就发生了:虽然instance指向了一个未被初始化的对象,但是它确实不为null了,所以这个判断会返回false,之后它将return一个未被初始化的单例对象!

由于重排序是编译器和CPU自动进行的,那么有什么办法能禁止这种重排序操作吗?很简单,给instance变量加个volatile关键字就行,这样编译器就会根据一定的规则禁止对volatile变量的读写操作重排序了。而编译出的字节码,也会在合适的地方插入内存屏障,比如volatile写操作之前和之后会分别插入一个StoreStore屏障和StoreLoad屏障,禁止CPU对指令的重排序越过这些屏障。

4. volatile的其他特性

对volatile变量的读写具有原子性,但是其他操作并不一定具有原子性,一个简单的例子就是i++。由于该操作并不具有原子性,故而即使该变量被volatile修饰,多线程环境下也不能保证线程安全。

5. 总结

volatile是JVM提供的轻量级同步工具。被volatile修饰的共享变量在多线程环境下可以获得可见性保证。其次它还能禁止指令重排。由于对volatile的写-读与锁的释放-获取具有相同的内存语义,故某些时候可以代替锁来获得更好的性能。但是和锁不一样,它不能保证任何时候都是线程安全的。

Java volatile关键字详解相关推荐

  1. Java——volatile关键字详解

    关注微信公众号:CodingTechWork,一起学习进步. volatile介绍 volatile概述 volatile是比synchronized关键字更轻量级的同步机制,访问volatile变量 ...

  2. 并发编程系列之volatile关键字详解

    并发编程系列之volatile关键字详解 1.volatile是什么? 首先简单说一下,volatile是什么?volatile是Java中的一个关键字,也是一种同步机制.volatile为了保证变量 ...

  3. java中实现具有传递性吗_Java中volatile关键字详解,jvm内存模型,原子性、可见性、有序性...

    一.Java内存模型 想要理解volatile为什么能确保可见性,就要先理解Java中的内存模型是什么样的. Java内存模型规定了所有的变量都存储在主内存中.每条线程中还有自己的工作内存,线程的工作 ...

  4. Java并发编程:volatile关键字详解

    volatile关键字两大特性:线程可见性/禁止指令重排序 原理:由jvm实现的一条汇编质量lock 要知道为什么会能保证线程的可见性,先要了解jmm的原子操作 假设一个变量initFlag默认为fa ...

  5. volatile 关键字详解

    volatile,可以当之无愧的被称为Java并发编程中"出现频率最高的关键字",常用于保持内存可见性和防止指令重排序. 保持内存可见性 内存可见性(Memory Visibili ...

  6. volatile关键字详解

    文章目录 1 volatile作用 2 volatile非原子的特性 3 原子类也并不完全安全 4 原子类和volatile区别 1 volatile作用 volatile关键字的主要作用是使变量在多 ...

  7. C语言volatile关键字详解

    volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据.如果没有volatile关键字,则编译器可能优化读取和存储 ...

  8. C/C++中volatile关键字详解

    1. 为什么用volatile? C/C++ 中的 volatile 关键字和 const 对应,用来修饰变量,通常用于建立语言级别的 memory barrier.这是 BS 在 "The ...

  9. Java instanceof关键字详解

    严格来说 instanceof 是 Java 中的一个双目运算符,由于它是由字母组成的,所以也是 Java 的保留关键字.在 Java 中可以使用 instanceof 关键字判断一个对象是否为一个类 ...

最新文章

  1. ASP.NET 2.0的异步页面刷新真给劲
  2. 记录一些使用git过程中的bug
  3. jquery --- 监听tab栏的变化
  4. UVA - 540:Team Queue
  5. java修改pdf内容流_java – 在PDFBox中,如何更改PDRectangle对象的原点(0,0)?
  6. 入侵微博服务器刷流量,开发者获刑 5 年;马化腾重回中国首富;支持 M1 芯片,VS Code 1.54 发布 | 极客头条...
  7. BackPropagation_01
  8. Linux之系统信息操作20170330
  9. 8086汇编语言(一) 汇编语言源程序
  10. Android截图功能
  11. B-spline Surfaces
  12. JavaScript基础之函数截流、防抖、柯理化
  13. Latex编辑器解决支持中文的问题
  14. java poi解析excel_Java 利用POI 解析Excel
  15. AMBER免费申请流程
  16. 使用cmd命令窗口打开对应的应用程序
  17. Hive 使用 Beeline 连接配置
  18. 北大2022计算机学院夏令营,2022保研夏令营:北京大学国家发展研究院夏令营活动...
  19. Python:实现double factorial iterative双阶乘迭代算法(附完整源码)
  20. maven项目中,使用pom文件引入自定义jar包

热门文章

  1. 为推广苹果音乐服务 库克再度现身北京
  2. 华为轮值董事长徐直军:5G不是原子弹 不伤害人
  3. 指针作为函数参数 进行内存释放 并置NULL
  4. windows安装npm教程
  5. c 文件操作_你电脑用久了,会有多少重复文件?快用它来整理一下吧
  6. 学音视频一定要掌握这几个算法
  7. java netty modbus协议接收iot数据
  8. python内置函数map_python内置函数 map/reduce
  9. 【kafka】kafka 执行 多个脚本 kafka-run-class.sh 导致 server 节点 时不时挂掉
  10. 【Flink】Flink 流计算 容错 source节点进行数据容错