Volatile 多线程中用到的关键字
前言
不管是在面试还是实际开发中 volatile 都是一个应该掌握的技能。
首先来看看为什么会出现这个关键字。
内存可见性
由于 Java 内存模型(JMM)规定,所有的变量都存放在主内存中,而每个线程都有着自己的工作内存(高速缓存)。
线程在工作时,需要将主内存中的数据拷贝到工作内存中。这样对数据的任何操作都是基于工作内存(效率提高),并且不能直接操作主内存以及其他线程工作内存中的数据,之后再将更新之后的数据刷新到主内存中。
这里所提到的主内存可以简单认为是堆内存,而工作内存则可以认为是栈内存。
如下图所示:

所以在并发运行时可能会出现线程 B 所读取到的数据是线程 A 更新之前的数据。
显然这肯定是会出问题的,因此 volatile 的作用出现了:
当一个变量被
volatile修饰时,任何线程对它的写操作都会立即刷新到主内存中,并且会强制让缓存了该变量的线程中的数据清空,必须从主内存重新读取最新数据。
volatile 修饰之后并不是让线程直接从主内存中获取数据,依然需要将变量拷贝到工作内存中。
内存可见性的应用
当我们需要在两个线程间依据主内存通信时,通信的那个变量就必须的用 volatile 来修饰:
public class Volatile implements Runnable{
private static volatile boolean flag = true ;
@Override
public void run() {
while (flag){
}
System.out.println(Thread.currentThread().getName() +"执行完毕");
}
public static void main(String[] args) throws InterruptedException {
Volatile aVolatile = new Volatile();
new Thread(aVolatile,"thread A").start();
System.out.println("main 线程正在运行") ;
Scanner sc = new Scanner(System.in);
while(sc.hasNext()){
String value = sc.next();
if(value.equals("1")){
new Thread(new Runnable() {
@Override
public void run() {
aVolatile.stopThread();
}
}).start();
break ;
}
}
System.out.println("主线程退出了!");
}
private void stopThread(){
flag = false ;
}
}
但这里有个误区,这样的使用方式容易给人的感觉是:主线程在修改了标志位使得线程 A 立即停止,如果没有用 volatile 修饰,就有可能出现延迟。
对
volatile修饰的变量进行并发操作是线程安全的。
这里要重点强调,volatile 并不能保证线程安全性!
如下程序:
public class VolatileInc implements Runnable{
private static volatile int count = 0 ; //使用 volatile 修饰基本数据内存不能保证原子性
//private static AtomicInteger count = new AtomicInteger() ;
@Override
public void run() {
for (int i=0;i<10000 ;i++){
count ++ ;
//count.incrementAndGet() ;
}
}
public static void main(String[] args) throws InterruptedException {
VolatileInc volatileInc = new VolatileInc() ;
Thread t1 = new Thread(volatileInc,"t1") ;
Thread t2 = new Thread(volatileInc,"t2") ;
t1.start();
//t1.join();
t2.start();
//t2.join();
for (int i=0;i<10000 ;i++){
count ++ ;
//count.incrementAndGet();
}
System.out.println("最终Count="+count);
}
}
当我们三个线程(t1,t2,main)同时对一个 int 进行累加时会发现最终的值都会小于 30000。
这是因为虽然
volatile保证了内存可见性,每个线程拿到的值都是最新值,但count ++这个操作并不是原子的,这里面涉及到获取值、自增、赋值的操作并不能同时完成。
所以想到达到线程安全可以使这三个线程串行执行(其实就是单线程,没有发挥多线程的优势)。
也可以使用
synchronize或者是锁的方式来保证原子性。还可以用
Atomic包中AtomicInteger来替换int,它利用了CAS算法来保证了原子性。
指令重排
内存可见性只是 volatile 的其中一个语义,它还可以防止 JVM 进行指令重排优化。
举一个伪代码:
|
1
2
3
|
int a=10 ;//1
int b=20 ;//2
int c= a+b ;//3
|
一段特别简单的代码,理想情况下它的执行顺序是:1>2>3。但有可能经过 JVM 优化之后的执行顺序变为了 2>1>3。
可以发现不管 JVM 怎么优化,前提都是保证单线程中最终结果不变的情况下进行的。
可能这里还看不出有什么问题,那看下一段伪代码:
private static Map<String,String> value ;
private static volatile boolean flag = fasle ;
//以下方法发生在线程 A 中 初始化 Map
public void initMap(){
//耗时操作
value = getMapValue() ;//
flag = true ;//
}
//发生在线程 B中 等到 Map 初始化成功进行其他操作
public void doSomeThing(){
while(!flag){
sleep() ;
}
//dosomething
doSomeThing(value);
}
所以加上 volatile 之后可以防止这样的重排优化,保证业务的正确性。
指令重排的的应用
一个经典的使用场景就是双重懒加载的单例模式了:
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
//防止指令重排
singleton = new Singleton();
}
}
}
return singleton;
}
}
如果不用 ,singleton = new Singleton();,这段代码其实是分为三步:
- 分配内存空间。(1)
- 初始化对象。(2)
- 将
singleton对象指向分配的内存地址。(3)
加上 volatile 是为了让以上的三步操作顺序执行,反之有可能第二步在第三步之前被执行就有可能某个线程拿到的单例对象是还没有初始化的,以致于报错。
总结
volatile 在 Java 并发中用的很多,比如像 Atomic 包中的 value、以及 AbstractQueuedLongSynchronizer 中的 state 都是被定义为 volatile 来用于保证内存可见性。
将这块理解透彻对我们编写并发程序时可以提供很大帮助。
转自:https://crossoverjie.top/2018/03/09/volatile/
Volatile 多线程中用到的关键字的更多相关文章
- 并发编程之ThreadLocal、Volatile、synchronized、Atomic关键字扫盲
前言 对于ThreadLocal.Volatile.synchronized.Atomic这四个关键字,我想一提及到大家肯定都想到的是解决在多线程并发环境下资源的共享问题,但是要细说每一个的特点.区别 ...
- Java多线程4:synchronized关键字
原文:http://www.cnblogs.com/skywang12345/p/3479202.html 1. synchronized原理在java中,每一个对象有且仅有一个同步锁.这也意味着,同 ...
- Java多线程:线程同步与关键字synchronized
一.同步的特性1. 不必同步类中所有的方法, 类可以同时拥有同步和非同步方法.2. 如果线程拥有同步和非同步方法, 则非同步方法可以被多个线程自由访问而不受锁的限制. 参见实验1:http://blo ...
- java多线程基础(synchronize关键字)
[toc] 基础知识 ---- 线程:进程(process)就是一块包含了某些资源的内存区域.操作系统利用进程把它的工作划分为一些功能单元. 线程:进程中所包含的一个或多个执行单元称为线程(threa ...
- [java基础]一文理解java多线程必备的sychronized关键字,从此不再混淆!
java并发编程中最长用到的关键字就是synchronized了,这里讲解一下这个关键字的用法和容易混淆的地方. synchronized关键字涉及到锁的概念, 在java中,synchronized ...
- ruby中的多线程和函数的关键字传参
1.实现ruby中的多线程 # def test1 # n = 1 # if n > 10 # puts "test1结束" # else # while true # sl ...
- C++多线程中用临界区控制全局变量的访问冲突问题
困扰了我很长时间的多线程访问全局变量今天终于解决了,所以得记录一下..控制全局变量的方法很多,有信号量.临界区等..这里我记录一个用临界区控制访问冲突的例子.非常好用. #include <wi ...
- java多线程中用到的方法详细解析
在多线程学习的过程中涉及的方法和接口特别多,本文就详细讲解下经常使用方法的作用和使用场景. 1.sleep()方法. 当线程对象调用sleep(time)方法后,当前线程会等待指定的时间(t ...
- java线程池 多线程搜索文件包含关键字所在的文件路径
文件读取和操作类 import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; publi ...
随机推荐
- Java中UML图
原文出处:http://blog.sina.com.cn/s/blog_7b3a17130100r95y.html 类(Class) 类(图A)是对象的蓝图,其中包含3个组成部分.第一个是Java中定 ...
- XXXX公司微课大赛技术储备
XXXX公司微课大赛技术储备 发短信验证 http://www.yunpian.com/ 发邮件 http://sendcloud.sohu.com/ flash头像上传组件 http://www.h ...
- Redis设置记录
首先大前提是,这个redis使用的外网端口,需要在防火墙或者安全组中打开 正常在redis配置文件里有个bind,这个默认是127.0.0.1,如果不修改,就是内网可以访问. 这里有个点需要提一下,就 ...
- Codeforces Round #320 (Div. 2) [Bayan Thanks-Round] A. Raising Bacteria【位运算/二进制拆分/细胞繁殖,每天倍增】
A. Raising Bacteria time limit per test 1 second memory limit per test 256 megabytes input standard ...
- Codeforces 581F Zublicanes and Mumocrates(树型DP)
题目链接 Round 322 Problem F 题意 给定一棵树,保证叶子结点个数为$2$(也就是度数为$1$的结点),现在要把所有的点染色(黑或白) 要求一半叶子结点的颜色为白,一半叶子结点的 ...
- CentOS 7.0 服务管理 – systemctl 命令
转载自:http://linux.it.net.cn/CentOS/fast/2014/0720/3212.html CentOS 7.0中已经没有service命令,而是启用了systemctl服务 ...
- devexpress XtraReport报表 ---主从报表
private void GetWqpWorkPrinter(List<ReelListMagt> list, string WorkDate, User user) { ReelList ...
- Android中Context详解 ---- 你所不知道的Context(转)
Android中Context详解 ---- 你所不知道的Context(转) 本文出处 :http://b ...
- tc: 模拟网络异常的工具-----------鸟窝
http://colobu.com/2017/04/21/tc-introduction/
- DATASNAP数据提交之FIREDAC的TFDJSONDeltas
DATASNAP数据提交之FIREDAC的TFDJSONDeltas FIREDAC的TFDJSONDeltas相当于CLIENTDATASET的DELTA,是作为CLIENTDATASET.DELT ...