Java并发编程——为什么要用volatile关键字
首发地址 https://blog.leapmie.com/archives/66ba646f/
日常编程中出现 volatile 关键字的频率并不高,大家可能对 volatile 关键字比较陌生,再深入一点也许是听闻 volatile 只能保证可见性而不能保证原子性,无法有效保证线程安全,于是更加避免使用 volatile ,简简单单加上synchronize关键字就完事了。本文稍微深入探讨 volatile 关键字,分析其作用及对应的使用场景。
并发编程的几个概念简述
首先简单介绍几个与并发编程相关的概念:
可见性
可见性是指变量在线程之间是否可见,JVM 中默认情况下线程之间不具备可见性。
原子性
对于 a = 0 操作是属于原子操作,但 a = a + 1 则不是原子操作,因为这里涉及到要先读取原来 a 的值,然后再为 a 加 1 ,当涉及多线程同时执行该语句时,会出现值不稳定的情况,所以非原子操作在并发场景下是不安全的。
有序性
java 内存模型中允许编译器和处理器进行指令重排优化,重排过程中不会影响单个线程的指令执行顺序,但会影响多线程环境中的运行正确性
指令重排
在多核 CPU 的情况下,为了充分利用时间片,提高指令执行效率,处理器会根据一定规则对指令进行重排序,由于规则的限定,指令重排后理论上最终运行结果不变。
volatile 的主要作用
volatile 的主要作用是实现可见性 和禁止指令重排
实现可见性
在 JVM 内存模型中内存分为主内存和工作内存,各线程有独自的工作内存,对于要操作的数据会从主内存拷贝一份到工作内存中,默认情况下工作内存是相互独立的,也就是线程之间不可见,而 volatile 最重要的作用之一就是使变量实现可见性。
禁止指令重排
虽然指令重排理论上不会影响执行结果的正确性,但指令重排只能保证底层的机器语言重排序后结果正确,而对于Java高级语言,所以在没有干预的情况下并不能确保每条语句在编译对应的指令重排后与期望的执行效果一致。
对于以下示例,由于 ready 没有指定 volatile ,当变量 ready 线程间不可见时,可能导致线程中读不到 ready 的新值,无法停止循环;如果指令重排序,可能在线程执行前变量 ready 已赋值为 true ,导致线程内容不打印。
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
@Override
public void run() {
while(!ready) {
Thread.yield();
}
System.out.println("1");
}
}
public static void main(String[] args) {
new ReaderThread().start();
ready = true;
}
}
为什么volatile不能保证线程安全?
想要线程安全必须保证原子性,可见性,有序性,而 volatile 只能保证可见性和有序性。
volatile 字段主要是让线程从主内存中获取值从而保证可见性,但是CPU中还有一层高速缓存——寄存器,对于非原子性操作,在底层指令运算中还是会出现数据缓存导致运算结果不正确的情况,从而无法保证线程安全。
简单来说,volatile 在多 cpu 环境下不能保证其它 cpu 的缓存同步刷新,因此无法保证原子性。
为什么不直接用synchronized
synchronized 可保证原子性、可见性、有序性,能有效保证线程安全,但是有个缺点是性能开销较大,而 volatile 是轻量级的线程安全实现方案,在某些特定场合下也能保证线程安全。由于 synchronized 的便捷性,也容易导致 synchronized 的滥用。
双重检查锁
因为 volatile 不能简易的实现线程安全,需要有较深入的了解才能正确使用,所以 volatile也显得更为复杂,使用频率也较低,而 volatile 的一个典型使用例子是双重检查锁模式。
双重检查锁通常用于单例模式或延迟赋值的场景,其代码通常如下
public class Singleton {
private volatile static Singleton uniqueSingleton; // 1. 为变量添加volatile修饰符
private Singleton() {
}
public Singleton getInstance() {
if (null == uniqueSingleton) { //2. 第一重检查
synchronized (Singleton.class) { // 3. synchronized加锁
if (null == uniqueSingleton) { // 4. 第二重检查
uniqueSingleton = new Singleton();
}
}
}
return uniqueSingleton;
}
}
以下是对这段代码的一些疑问及解答:
Q: 为什么不在 getInstance 方法直接加 synchronized ?
A: 只有在第一次初始化时才需要加锁,如果在getInstance方法上加锁则每次获取实例时都会对整段代码块加锁,影响性能
Q: 为什么需要双重检查?
A: 如果多线程同时通过了第一次检查,其中一个线程需要通过了第二次检查才进行实例化对象,其余线程在后续等待获取到锁后则判断到变量非空,跳过赋值操作。
Q: 为什么 uniqueSingleton 需要添加volatile关键字?
A: 对于 uniqueSingleton = new Singleton();语句,实际上可以分解成以下三个步骤:
- 分配内存空间
- 初始化对象
- 将对象指向刚分配的内存空间
但是有些编译器为了性能的原因,可能会将第二步和第三步进行重排序,顺序就成了:
- 分配内存空间
- 将对象指向刚分配的内存空间
- 初始化对象
现在考虑重排序后,两个线程发生了以下调用:
| Time | Thread A | Thread B |
|---|---|---|
| T1 | 检查到uniqueSingleton为空 | |
| T2 | 获取锁 | |
| T3 | 再次检查到uniqueSingleton为空 | |
| T4 | 为uniqueSingleton分配内存空间 | |
| T5 | 将uniqueSingleton指向内存空间 | |
| T6 | 检查到uniqueSingleton不为空 | |
| T7 | 访问uniqueSingleton(此时对象还未完成初始化) | |
| T8 | 初始化uniqueSingleton |
在这里添加volatile关键字主要是避免在对象未完整完成对象创建就已经被其他线程读取,造成空指针异常。
总结
- volatile 的主要作用是实现可见性和禁止指令重排。
- 线程安全需要满足可见性、有序性、原子性。
- volatile 可以保证可见性和有序性,但是无法保证原子性,所以是线程不安全的。(非原子操作可能会导致数据缓存在CPU的cache中,产生数据不一致)
- synchronized 关键字虽然可以保证可见性、有序性、原子性,而且用法简单,但是性能开销大。
- 双重检查锁模式是 volatile 的典型使用场景,双重检查锁通常用于实现单例模式或延迟赋值。
参考
Java并发编程——为什么要用volatile关键字的更多相关文章
- Java并发编程(六)volatile关键字解析
由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我们先来了解一下与内存模型相关的概念和知识. 一.内存模型的相关概念 Java内存模型规定所有的变量都是存在 ...
- Java并发编程:JMM和volatile关键字
转载请标明出处: http://blog.csdn.net/forezp/article/details/77580491 本文出自方志朋的博客 Java内存模型 随着计算机的CPU的飞速发展,CPU ...
- 【Java并发编程】6、volatile关键字解析&内存模型&并发编程中三概念
volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在Java 5之后,volatile关键字才得以 ...
- 【Java并发编程】11、volatile的使用及其原理
一.volatile的作用 在<Java并发编程:核心理论>一文中,我们已经提到过可见性.有序性及原子性问题,通常情况下我们可以通过Synchronized关键字来解决这些个问题,不过如果 ...
- Java并发编程的艺术(三)——volatile
1. 并发编程的两个关键问题 并发是让多个线程同时执行,若线程之间是独立的,那并发实现起来很简单,各自执行各自的就行:但往往多条线程之间需要共享数据,此时在并发编程过程中就不可避免要考虑两个问题:通信 ...
- Java并发编程(三)volatile域
相关文章 Java并发编程(一)线程定义.状态和属性 Java并发编程(二)同步 Android多线程(一)线程池 Android多线程(二)AsyncTask源代码分析 前言 有时仅仅为了读写一个或 ...
- Java并发机制(3)--volatile关键字与内存模型
Java并发编程:volatile关键字解析及内存模型 个人整理自:博客园-海子-http://www.cnblogs.com/dolphin0520/p/3920373.html 1.线程内存模型: ...
- Java并发编程底层实现原理 - volatile
Java语言规范第三版中对volatile的定义如下: Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致性的更新,线程应该确保通过排他锁 单独获得这个变量. volatile有时候 ...
- java并发编程系列七:volatile和sinchronized底层实现原理
一.线程安全 1. 怎样让多线程下的类安全起来 无状态.加锁.让类不可变.栈封闭.安全的发布对象 2. 死锁 2.1 死锁概念及解决死锁的原则 一定发生在多个线程争夺多个资源里的情况下,发生的原因是 ...
随机推荐
- HDFS ha 格式化报错:a shared edits dir must not be specified if HA is not enabled.
错误内容: Formatting using clusterid: CID-19921335-620f-4e72-a056-899702613a6b2019-01-12 07:28:46,986 IN ...
- MIPI CSI-2
目录 1 MIPI简介 2 MIPI CSI-2简介 2.1 MIPI CSI-2 的层次结构 2.2 CSI-2协议层 2.3 打包/解包层 2.4 LLP(Low Level Protocol)层 ...
- go语言实现"生产者"和"消费者"的例子
学习java的多线程的时候最经典的一个例子就是生产者消费者模型的例子,最近在研究go语言协程,发现go提供的sync包中有很多和java类似的锁工具,尝试着用锁工具配合协程实现一个"消费者& ...
- 第一个SpringMVC程序 (注解版)
1.新建一个web项目 2.导入相关jar包 3.编写web.xml , 注册DispatcherServlet <?xml version="1.0" encoding=& ...
- 这一次搞懂Spring的XML解析原理
前言 Spring已经是我们Java Web开发必不可少的一个框架,其大大简化了我们的开发,提高了开发者的效率.同时,其源码对于开发者来说也是宝藏,从中我们可以学习到非常优秀的设计思想以及优雅的命名规 ...
- cb48a_c++_STL_算法_重排和分区random_shuffle_stable_partition
cb48a_c++_STL_算法_重排和分区random_shuffle_stable_partition random_shuffle()//重排,随机重排,打乱顺序 partition()分区,把 ...
- ca75a_c++_标准IO库-利用流对象把文件内容读取到向量-操作文件
/*ca75a_c++_标准IO库习题练习习题8.3,8.4,8.6习题8.9.8.10 ifstream inFile(fileName.c_str());1>d:\users\txwtech ...
- Python数据处理常用工具(pandas)
目录 数据清洗的常用工具--Pandas 数据清洗的常用工具 Pandas常用数据结构series和方法 Pandas常用数据结构dataframe和方法 常用方法 数据清洗的常用工具--Pandas ...
- 分享2个近期遇到的MySQL数据库的BUG案例
近一个月处理历史数据问题时,居然连续遇到了2个MySQL BUG,分享给大家一下,也欢迎指正是否有问题. BUG1: 数据库版本: MySQL5.7.25 - 28 操作系统: Centos 7.7 ...
- Java多线程之内存模型
目录 多线程需要解决的问题 线程之间的通信 线程之间的通信 Java内存模型 内存间的交互操作 指令屏障 happens-before规则 指令重排序 从源程序到字节指令的重排序 as-if-seri ...