深入理解volatile(整理自尚硅谷阳哥版本)
1、谈谈你对volatile的理解?
volatile是Java虚拟机提供的轻量级的同步机制
-
保证可见性
-
不保证原子性
-
禁止指令重排
2、JMM(内存模型)你谈谈?—— 可见性、原子性、有序性
使得线程安全性获得保证
JMM(Java内存模型Java Memory Model) 本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式
JMM关于同步的规定:
-
线程解锁前,必须把共享变量的值刷新回主内存
-
线程加锁前,必须读取主内存的最新值到自己的工作内存
-
加锁解锁是一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间通信(传值)必须通过主内存来完成,其简要访问过程如下图:
class MyData{// MyData.java ==> myData.class ==> JVM字节码 volatile int number = 0; public void addT060(){ this.number = 60; } //请注意,此时number前面是加了volatile关键字修饰的,volatile不保证原子性 public void addPlusPlus(){ number++; } AtomicInteger atomicInteger = new AutomicInteger(); public void addMyAtomic(){ atomicInteger.getAndIncrement(); }}//1. 验证volatile的可见性// 1.1 加入int number = 0; number变量之前根本没有添加volatile关键字修饰 没有可见性//1.2 添加了volatile,可以解决可见性问题//2 验证volatile不保证原子性//2.1 原子性指的是什么意思? —— 不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。需要整体完整,要么同时成功,要么同时失败//2.2 volatile不保证原子性的案例演示//2.3 why? //2.4 如何解决原子性?// 加sync 使用juc下AtomicIntegerpublic class VolatileDemo{ public static void main(String[] args){ myData myData = new MyData(); for(int i = 1; i { for(int j = 1; i 2){//main 和 gc 线程 //暂停当前正在运行的线程对象,并执行其他线程 Thread.yield(); } Syetem.out.println(Thread.currentThread().getName()+"\t int type,finally number value"+myData.number); Syetem.out.println(Thread.currentThread().getName()+"\t AtomicInteger type, finally number value"+myData.number); } //volatile可以保证可见性,及时通知其他线程,主物理内存的值已经被修改 public static void seeOkByVolatile(){ MyData myData = new MyData();//资源类 new Thread(() -> { Syetem.out.println(Thread.currentThread().getName()+"\t come in"); try{//暂停一会线程 TimeUnit.SECONDS.sleep(3); }catch(InterruptedException e){ e.printStackTrace(); } myData.addT060(); Syetem.out.println(Thread.currentThread().getName()+"\t updated number value:"+myData.number); },"AAA").start(); //第2个线程就是我们的main线程 while(myData.number == 0){ //main线程就一直在这里等待循环,直到number值不再等于0 } Syetem.out.println(Thread.currentThread().getName()+"\t mission is over,mian get number value: "+myData.number); }}
可见性就是及时通知机制,通过前面的介绍,我们知道各个线程对主内存中共享变量的操作都是各自线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的。 这就可能存在一个线程A修改了共享变量C的值但还未写回主内存时,另外一个线程B又对主内存中同一个共享变量X进行操作,但此时A线程工作内存中共享变量X对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题
有序性:计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分为以下3种:
单线程环境里面确保程序最终执行结果和代码顺序执行结果一致。 处理器在进行重排序时必须要考虑指令之间的数据依赖性 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测
public void sort(){ int x = 11; int y = 12; x = x + 5; y = x * x; //可能执行的顺序:1234 2134 1324 //问题:语句4可以变成第一条吗 不能 //因为处理器在进行重排序的时候必须要考虑指令之间的数据依赖性}
重排案例2图
public class ReSortSeqDemo{ int a = 0; boolean flag = false; public void method1(){ a = 1;//语句1 flag = true;//语句2 } //多线程环境中线程交替执行,由于编译器优化重排的存在, //两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测 public void method02(){ if(flag){ a = a + 5;//语句3 System.out.println("*****retValue:"+a); } }}
禁止指令重排小总结(了解)
volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象 先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它有两个作用:
-
保证特定操作的执行顺序
-
保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
线程安全性获得保证
-
工作内存与主内存同步延迟现象导致的可见性问题:可以使用synchronized或volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见
-
对于指令重排导致的可见性问题和有序性问题:可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化
3、你在哪些地方用到过volatile?
3.1 单例模式DCL代码 3.2 单例模式volatile分析
public class SingletonDemo{ private static volatile SingletonDemo instance = null; private SingletonDemo(){ Syetem.out.println(Thread.currentThread().getName()+"\t 我是构造方法SingletonDemo()"); }//DCL(Double Check Lock双端检锁机制) public static SingletonDemo getInstance(){ if(instance == null){ synchronized(SingletonDemo.class){ if(instance == null){ instance = new SingletonDemo(); } } } return instance; } public static void main(String[] args){ //单线程(main线程的操作动作.....) //System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); //System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); //System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); //并发多线程后,情况发生了很大的变化 for(int i = 1; i { SingletonDemo.getInstance(); },String.valueOf(i)).start(); } }}
DCL(双端检锁)机制不一定线程安全,原因是有指令重排序的存在,加入volatile可以禁止指令重排
原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。 instance = new SingletonDemo(); 可以分为以下3步完成(伪代码)
memory = allocate();//1. 分配对象内存空间 instance = (memory);//2. 初始化对象 instance = memory; //3. 设置instance指向刚分配的内存地址,此时instance != null (例子:插班生张三)
步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重拍后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。
memory = allocate();//1. 分配对象内存空间 instance = memory; //3. 设置instance指向刚分配的内存地址,此时instance != null,但是对象还没初始化完成! instance(memory); //2. 初始化对象
但是指令重排只会保证串行语句的执行的一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。