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 死锁概念及解决死锁的原则 一定发生在多个线程争夺多个资源里的情况下,发生的原因是 ...
随机推荐
- ElasticSearch系列(二):ElasticSearch Head、Kibana、Elasticsearch-Analysis-Ik安装、使用
1.ElasticSearch Head使用 扩展程序安装插件:ElasticSearch github地址:https://github.com/mobz/elasticsearch-head/ 运 ...
- TD课程通的最终版评价
相较之前的软件,功能得到了完善,同时也修复了BUG,还增加了辅助工具,可以查询一周的占用情况,省空间,很方便. 添加课程表的功能也得到了完善,同时增加了链接到教务系统的功能,方便查询相关的课程.问题是 ...
- 简单5步,轻松debug K8S服务!
作者: Ram Rai,性能.可扩展性以及软件架构的爱好者 原文链接: https://medium.com/better-programming/debug-your-kubernetes-serv ...
- ios支付签名认证
一.解析json中参数的含义 private Long id; @ApiModelProperty(value = "创建时间") private Date gmtCreate; ...
- UI 小白设计常用的Website
网址 设计师导航网址(其实这个用好了,enough) 设计导航1 设计导航2 icon 阿里巴巴Iconfont flaticons icomoon Noun Project 图片(有时候就是灵魂) ...
- CenterOS7 修改 SSH 端口
首先修改 /etc/ssh/sshd_config 文件中的 Port.修改前一定要备份 可以同时启用多个Port所以最好先追加一个端口,新端口校验没问题之后再把原端口删除 防火墙设置 # 永久开放端 ...
- [转] 浅谈C++中的那些内存泄露
点击阅读原文 尽管学过C语言.可是C++里面的一些基础还是不太懂,还须要再掌握. 对于内存泄露,我的个人理解就是程序在执行过程中,自己开辟了空间,用完这块空间后却没有释放. 今晚上我就犯了这种低级错误 ...
- [noi.ac省选模拟赛]第12场题解集合
题目 比赛界面. T1 数据范围明示直接\(O(n^2)\)计算,问题就在如何快速计算. 树上路径统计通常会用到差分方法.这里有两棵树,因此我们可以做"差分套差分",在 A 树上对 ...
- iphone手机卡顿解决方案
一.清除 safari缓存 设置->safari浏览器->清除历史记录与网站数据
- MSSQL2008下备份好的*.bak--->>>恢复到--->>>MSSQL2014(解决办法)
MSSQL2008下备份好的*.bak--->>>恢复到--->>>MSSQL2014(解决办法) 在进行CTE训练时(同时也要理解下窗口函数的应用),发现不能继续 ...