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 死锁概念及解决死锁的原则 一定发生在多个线程争夺多个资源里的情况下,发生的原因是 ...
随机推荐
- java关键字volatile用法详解
volatile关键字想必大家都不陌生,在java 5之前有着挺大的争议,在java 5之后才逐渐被大家接受,同时作为java的关键字之一,其作用自然是不可小觑的,要知道它是java.util.con ...
- SpringSceurity(3)---图形验证码功能实现
SpringSceurity(3)---图形验证码功能实现 有关springSceurity之前有写过两篇文章: 1.SpringSecurity(1)---认证+授权代码实现 2.SpringSec ...
- 曹工说JDK源码(2)--ConcurrentHashMap的多线程扩容,说白了,就是分段取任务
前言 先预先说明,我这边jdk的代码版本为1.8.0_11,同时,因为我直接在本地jdk源码上进行了部分修改.调试,所以,导致大家看到的我这边贴的代码,和大家的不太一样. 不过,我对源码进行修改.重构 ...
- iOS-函数式编程 && 响应式编程概念
作为一个iOS developer,那么你一定用过Masnory / SnapKit: Masonry是一个OC开发中,非常好用的自动布局的第三方框架: SnapKit是Masonry团队打造的swi ...
- 分布式数据库PolonDB 云端发力未来数据处理需求
企业数字化转型的不断深入,传统 IT 架构和数据库早已无法适应诸如物联网.新金融.新零售.新制造等行业对于数据高吞吐.灵活扩展等需求,企业对数据库有了更高的要求. 青云QingCloud 本次推出的 ...
- Python--函数&过程
函数式编程与过程式编程打的区分:过程是没有返回值的函数,过程在python3中也有返回值,为None 函数的作用:代码复用.保持代码的一致性.使代码更容易扩展 过程的定义与调用: 1 def func ...
- 关于"touchstart与click同时触发"问题
点击事件可以分解成多个事件: 在移动端,手指点击一个元素,会经过:touchstart --> touchmove -> touchend --> click 由于移动设备能够同时 ...
- MySQL数据库基础知识复习
现在是2020年寒假,这也是新年写的第一篇博客,用了十几天的时间自学了数据库基础部分,想总结一下得失同时并通过写博客来复习前面学的知识点. 个人: 1.本来是计划一周学完基础部分的178p但没能完成这 ...
- 快捷键浏览存储过程的内容(执行文中的User Store Proc,设置快捷方式的指向usp_Name)
虽然不常用,但使用时还是在用传统方法查看存储过程的内容, sp_helptext usp_Name;下面这段执行后可设置快捷查询usp的内容(但是需要在每个数据库下都存在,不能直接在master,并且 ...
- bug的描述
我们知道了自身的症状,那么就从这里开始,一起聊一聊一个优秀的 BUG,应该包含哪些方面的内容呢? 标题 其实每一个 BUG 也都是一个小的文档,既然是文档,我们首先就要做好一个 “标题党”,当然,此 ...