JUC并发编程(10)--- 谈谈对Volatile的理解
Posted 小样5411
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC并发编程(10)--- 谈谈对Volatile的理解相关的知识,希望对你有一定的参考价值。
前言
什么是Volatile?
答:Volatile是Java虚拟机提供的轻量级同步机制
Volatile的三大特性:
1、保证可见性
2、不保证原子性
3、禁止指令重排
一、保证可见性
讲可见性就不得不提JMM,我们先来了解什么是JMM?
JMM:java内存模型,就是一个约定、规范,实际不存在。
关于JMM的一些同步约定
1、线程加锁前,必须读取主内存中的最新值到工作内存中
2、线程解锁前,必须把共享变量立刻刷回主存
3、加锁和解锁是同一把锁
我通过线程运行机制了解它
图中8种操作即线程运行的8种操作
分析:如图,首先加锁前要将主存最新值读取到工作内存,通过read和load操作,读取到工作内存就需要处理执行,use和assign(执行和返回),解锁前通过write和store操作刷回主存。过程就是read和load,lock,use和assign,write和store,unlock,一共8个操作,其中红色框框出的操作都是成对执行的
存在问题:线程A和线程B同时执行时,线程B将主存中Flag=true刷新成了Flag=false,但线程A不能及时可见,还用的是true,就是不知道线程B将Flag改变了,于是就要用到Volatile,保证可见性,让线程A看得到除自己以外的线程B操作,进而知道Flag被改变了
我们可以看下面代码再具体理解
package com.yx.JMM;
import java.util.concurrent.TimeUnit;
public class JMMDemo {
private static boolean Flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{//线程A
while (Flag==true){
}
},"A").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{//线程B
change();
},"B").start();
}
public static void change(){
Flag=false;
System.out.println(Flag);
}
}
线程A先执行,进入循环,然后1秒后线程B执行调用change方法,打印改变后的Flag,即false,但是程序一直不停止,陷入死循环。因为线程B改变了Flag,线程A不可见,所以依然认为Flag=true,一直在循环。
我们加一个volatile修饰,再运行程序,就不会出现死循环了,正常结束
这就是volatile保证可见性,线程B改变,线程A也可见。不然还可能因为线程B中途修改了主存中的值,但是线程A不知道,处理完又把自己的变量写回主存,造成和修改之前一样,相当于线程B没有修改。比如初始Flag=true,然后线程A读取后,线程B再来将其修改为false,但线程A不知道,最后又把Flag变为true,这就相当于没变了。
二、不保证原子性
什么是原子性?
原子性就是不可分割,如线程A在执行时,不能被打扰,也不能被分割
我们来看一个案例
package com.yx.JMM;
public class vDemo {
//volatile不保证原子性
private volatile static int num = 0;
public static void add(){
num++;
}
public static void main(String[] args) {
//理论应该是2w
//执行20个线程,每个都加1000
for (int i = 0 ; i < 20 ; i++){
new Thread(()->{
for (int j = 0 ; j < 1000 ; j++){
add();
}
}).start();
}
while (Thread.activeCount()>2){//至少有main和GC线程
Thread.yield();//线程让步
}
System.out.println(num);
}
}
理论上有20000,但怎么执行都到不了2w
为什么会这样呢?
因为add()中的num++就不是原子操作,会被多个线程占用从而不自增
我们来将它的字节码,变成汇编形式
可以看到,一个num++在汇编下,有多行命令,
getstatic表示获取原始值,iadd表示+1,putstatic表示将结果写回,
也就是说它本身在JVM中并不是原子性的操作,多线程情况下从主存中取值(write和load),
在线程工作内存运算,写回主存的时候可能会出现覆写情况,所以最终结果无法到20000
如何保证原子性?
synchronized和lock肯定能保证原子性,但比较耗费资源,有没有更优的方法
如果不加synchronized和lock怎么保证原子性?
用上面这个atomic类就可以,用原子类的int,就是AtomicInteger,更安全,高效
package com.yx.JMM;
import java.util.concurrent.atomic.AtomicInteger;
public class vDemo {
//volatile不保证原子性
private volatile static AtomicInteger num = new AtomicInteger();
public static void add(){
num.getAndIncrement();//AtomicInteger+1,用的是CAS:cpu的并发原语,效率极高
}
public static void main(String[] args) {
//理论应该是2w
//执行20个线程,每个都加1000
for (int i = 0 ; i < 20 ; i++){
new Thread(()->{
for (int j = 0 ; j < 1000 ; j++){
add();
}
}).start();
}
while (Thread.activeCount()>2){//至少有main和GC线程
Thread.yield();//线程让步
}
System.out.println(num);
}
}
正常结果20000
num.getAndIncrement()十分高效,并且有别于num++,它是在底层,在内存中修改值,这涉及CAS,后面专门发文讲。
三、禁止指令重排
指令重排:计算机并不是按你写的程序那样执行的,处理器会考虑指令之间的依赖性来重排
源代码->编译器优化重排->指令并行也会重排->内存系统也会重排->执行
如上图,指令重排,可能会导致指令执行顺序不一致,导致赋值或者逻辑不一致,从而结果异常。虽然指令重排造成错误情况出现几率极少,1000万次重排,都不会出现一次,但理论上可能出现,我们需要了解下这个。
volatile如何避免指令重排?
有内存屏障,它就是一个CPU指令,可以保证特定操作的执行顺序,因为它可以禁止指令间的顺序颠倒,如图,只要加了volatile就会有内存屏障,禁止执行顺序交换、重排
总结:volatile可以保证可见性,不能保证原子性,避免出现指令重排现象
以上是关于JUC并发编程(10)--- 谈谈对Volatile的理解的主要内容,如果未能解决你的问题,请参考以下文章
你真的懂并发吗?谈谈对JUC线程池ThreadPoolExecutor的认识吧