Java 并发之 volatile 关键字全面总结 迷南。 2022-11-15 07:01 119阅读 0赞 # 一、简介 # volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。 # 二、并发编程的3个基本概念 # ### 1.原子性 ### 定义: **即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。** 原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。Java中的原子性操作包括: (1)基本类型的读取和赋值操作,且赋值必须是值赋给变量,变量之间的相互赋值不是原子性操作。 (2)所有引用(reference)的赋值操作 (3)java.concurrent.Atomic.\* 包中所有类的一切操作 ### 2.可见性 ### 定义:**指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。** 在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取(**也就是说volatile可以保证被修饰的变量的内存可见性**)。当然,synchronized和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁,然后执行同步代码,并且在释放锁之前,会将对变量的修改刷新到主存当中。因此可以保证可见性。 ### 3.有序性 ### 定义:**即程序执行的顺序,按照代码的先后顺序执行。** Java内存模型中的有序性可以总结为:**如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。**前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存和主内存同步延迟”现象。 在Java内存模型(JMM)中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行结果,但是对多线程会有影响。Java提供volatile来保证一定的有序性(**即volatile禁止指令重排序**)。最著名的例子就是单例模式里面的DoubleCheckLock(双重检查锁)。另外,可以通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。 ## 三、锁的互斥和可见性 ## 锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。 (1)互斥即一次只允许一个线程持有某个特定的锁,一次就只有一个线程能够使用该共享数据。 (2)可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的。也即**当一条线程修改了共享变量的值,新值对于其他线程来说是可以立即得知的**。如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件: * 对变量的写操作不依赖于当前值。 * 该变量没有包含在具有其他变量的不变式中。 实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。事实上就是:保证**操作是原子性操作**,才能保证使用**volatile关键字**的程序在并发时**能**够**线程安全**的正确执行。(这个很不妥,不能寄希望于volatile来保证线程安全。) ## 四、Java的内存模型(JMM)以及共享变量的可见性 ## JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的共享变量的拷贝副本,线程对变量的所有操作都必须在工作内存(即local 内存)中进行,而不能直接读写主内存中的变量。不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。 ![watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3MDkzNDY1_size_16_color_FFFFFF_t_70][] 对于普通的共享变量来讲,线程A将其修改为某个值,修改过程发生在线程A的本地内存中,此时还未同步到主内存中去;而线程B已经缓存了该变量的旧值,所以就导致了共享变量值的不一致。解决这种共享变量在多线程模型中的不一致问题(即不可见性问题),粗暴的方式自然就是加锁,但是此处使用synchronized或者Lock这些方式太重量级了,比较合理的方式其实就是volatile。 **需要注意的是,JMM是个抽象的Java内存模型,所以所谓的本地内存,主内存都是抽象概念,并不一定就真实的对应cpu缓存和物理内存** ## 五、volatile变量的特性 ## ### 1.保证可见性,不保证原子性 ### 1. 当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去; 2. 这个写操作会导致其他线程的local memory中的volatile变量缓存无效,必须再去main memory中获取一次。 ### 2.禁止指令重排 ### 指令重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。重排序需要遵守一定规则:![watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3MDkzNDY1_size_16_color_FFFFFF_t_70 1][] **(1)重排序操作不会对存在数据依赖关系的操作进行重排序。** 比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时,这两个操作不会被重排序。 **(2)重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变。** 比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系, 所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。 重排序在单线程下一定能保证结果的正确性,但是在多线程环境下,可能发生重排序,影响结果,下例中的1和2由于不存在数据依赖关系,就有如下可能:则有可能会被重排序,先执行status=true再执行a=2。而此时线程B会顺利到达4处,而线程A中a=2这个操作还未被执行,所以b=a+1的结果也有可能依然等于2。 public class Volatile_3 { int a = 1; boolean status = false; @Test public void reorder() { while (true) { new Thread(this::changeStatus).start(); new Thread(this::run).start(); } } /** * 状态切换为true */ public void changeStatus() { a = 2; //1 status = true; //2 } /** * 若状态为true,则为running */ public void run() { if (status) { //3 int b = a + 1; //4 if (b == 2) { System.out.println(b); } } } } 使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,volatile禁止指令重排序也有一些规则: 1. 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行; 2. 在进行指令优化时,不能将对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。 **即执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见。** ## 六、volatile不适用的场景 ## ### 1.volatile不适合复合操作 ### 例如,inc++不是一个原子性操作,可以由读取、加、赋值3步组成,所以结果有可能没达到10000。10个线程去操作,预期结果是1w,线程太少,可能多次都是正确结果,想要异常就得加线程数,提高并发度,然后就会出现计算结果与预期不一致的情况,下面会有不一致的截图奉上。 public class Volatile_1 { public volatile int inc = 0; public void increase() { inc++; } public static void main(String[] args) { final Volatile_1 test = new Volatile_1(); for (int i = 0; i < 10; i++) { new Thread() { @Override public void run() { for (int j = 0; j < 1000; j++) { test.increase(); System.out.println(test.inc); } } }.start(); } //保证前面的线程都执行完 while (Thread.activeCount() > 1) { Thread.yield(); } System.out.println(test.inc); } } ![watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3MDkzNDY1_size_16_color_FFFFFF_t_70 2][] 期望值是:1000 x 1000 = 1000000,但是图上的实际运行结果是999988,少了不少,因为现在电脑太厉害了,要是没错,那就再加线程数多运行几次。这也是为啥上面说volatile在某些情况下可以保证线程安全,但是这个某些情况,确实不是那么好说清楚的,所以我就不建议用这个volatile来保证线程安全。 ### 2.解决方法 ### (1)采用synchronized ![watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3MDkzNDY1_size_16_color_FFFFFF_t_70 3][] **为什么加锁后就保证了变量的内存可见性了?** 因为当一个线程进入 synchronizer 代码块后,线程获取到锁,会清空本地内存,然后从主内存中拷贝共享变量的最新值到本地内存作为副本,执行代码,又将修改后的副本值刷新到主内存中,最后线程释放锁。而获取不到锁的线程会阻塞等待,所以变量的值肯定一直都是最新的。 (2)采用Lock ![watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3MDkzNDY1_size_16_color_FFFFFF_t_70 4][] 除了 synchronizer 外,其它锁也能保证变量的内存可见性。 (3)采用java并发包中的原子操作类,原子操作类是通过CAS循环的方式来保证其原子性的 ![watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3MDkzNDY1_size_16_color_FFFFFF_t_70 5][] **在多线程环境下,volatile 关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性。也就是说,多线程环境下,使用 volatile 修饰的变量是线程不安全的。** ## 七、volatile原理 ## **volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。**在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能: 1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成; 2. 它会强制将对缓存的修改操作立即写入主存; 3. 如果是写操作,它会导致其他CPU中对应的缓存行无效。 ## 八、单例模式的双重锁为什么要加volatile ## public class TestInstance{ private volatile static TestInstance instance; public static TestInstance getInstance(){ //1 if(instance == null){ //2 synchronized(TestInstance.class){ //3 if(instance == null){ //4 instance = new TestInstance(); //5 } } } return instance; //6 } } 需要volatile关键字的原因是,在并发情况下,如果没有volatile关键字,在第5行会出现问题。instance = new TestInstance();可以分解为3行伪代码 a. memory = allocate() //分配内存 b. ctorInstanc(memory) //初始化对象 c. instance = memory //设置instance指向刚分配的地址 上面的代码在编译运行时,可能会出现重排序从a-b-c排序为a-c-b。在多线程的情况下会出现以下问题。当线程A在执行第5行代码时,B线程进来执行到第2行代码。假设此时A执行的过程中发生了指令重排序,即先执行了a和c,没有执行b。那么由于A线程执行了c导致instance指向了一段地址,所以B线程判断instance不为null,会直接跳到第6行并返回一个未初始化的对象。 ## 九,变量的内存可见性例子 ## package com.lxk.thread.volatileTest; import org.junit.Test; /** * Java 并发编程之 测试 可见性 * 一个线程对共享变量的操作对其他线程是不可见的 * * @author LiXuekai on 2021/3/31 */ public class Volatile_2 { /** * 测试效果: * 1,不打开那行打印的注释,则输出结果就是run里面的一次打印,for循环竟然不打印。 * 2,打开注释的那行打印代码,会稍微过一会儿,该打印的都打印了。 */ @Test public void test() { MyThread myThread = new MyThread(); // 开启线程 myThread.start(); // 主线程执行 while (true) { //System.out.println("s");//这个打开也会触发,醉了,怎么解释呢? if (myThread.isFlag()) { System.out.println("主线程访问到 flag 变量"); } } } } /** * 子线程类 */ class MyThread extends Thread { private boolean flag = false; @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // 修改变量值 flag = true; System.out.println("flag = " + flag); } /** * 打开和注释这行代码,对程序运行也有着bug级的影响,没点水平还真解释不了这个极为不常见的现象。 */ public boolean isFlag() { //System.out.println("调用了。。。" + flag); return flag; } public void setFlag(boolean flag) { this.flag = flag; } } *(代码里面有2行注释代码,没打开的情况下,这内存可见性理论是一切正常,可是一旦打开某一个,那现象就不好解释了,我暂时还没找到解释的剧本呢。)* 运行结果以及实际运行结果截图:就只是打印了一行输出,没有想象中的一直刷刷的打印信息。 ![watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3MDkzNDY1_size_16_color_FFFFFF_t_70 6][] 现象分析:**原因--在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的**,这个例子里面,虽然子线程改变了flag的状态,但是,因为2个线程互相不直接通信,都是根据jmm设计的规则,都操作本地缓存中都信息,子线程对共享变量修改了,但是2个线程没有保证可见性。这个时候就可以使用volatile来保证共享变量在线程之间的内存可见性,flag属性加上volatile修饰之后,运行结果就如最开始预期一般刷刷的打印了。使用 volatile 修饰共享变量后,每个线程要操作变量时会从主内存中将变量拷贝到本地内存作为副本,当线程的变量副本被修改了之后,jmm强制他写回主内存,其他线程会通过 **CPU 总线嗅探机制**得知该变量副本已经失效,需要重新从主内存中读取。volatile 保证了不同线程对共享变量操作的可见性,也就是说一个线程修改了 volatile 修饰的变量,当修改后的变量写回主内存时,其他线程能立即看到最新值。 ## 十,总线嗅探机制 ## 在现代计算机中,CPU 的速度是极高的,如果 CPU 需要存取数据时都直接与内存打交道,在存取过程中,CPU 将一直空闲,这是一种极大的浪费,所以,为了提高处理速度,CPU 不直接和内存进行通信,而是在 CPU 与内存之间加入很多寄存器,多级缓存,它们比内存的存取速度高得多,这样就解决了 CPU 运算速度和内存读取速度不一致问题。 由于 CPU 与内存之间加入了缓存,在进行数据操作时,先将数据从内存拷贝到缓存中,CPU 直接操作的是缓存中的数据。但在多处理器下,将可能导致各自的缓存数据不一致(这也是可见性问题的由来),为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,而**嗅探是实现缓存一致性的常见机制**。 ![watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3MDkzNDY1_size_16_color_FFFFFF_t_70 7][] 注意,缓存的一致性问题,不是多处理器导致,而是多缓存导致的。 **缓存一致性协议:**当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。 **嗅探机制工作原理**:每个处理器通过监听在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址修改,就会将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作的时候,会重新从主内存中把数据读到处理器缓存中。 注意:基于 CPU 缓存一致性协议,JVM 实现了 volatile 的可见性,但由于总线嗅探机制,会不断的监听总线,如果大量使用 volatile 会引起**总线风暴**。所以,volatile 的使用要适合具体场景。 ## 十一,volatile与synchronized的区别 ## * volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。 * volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);而synchronized是一种排他(互斥)的机制。 * volatile用于禁止指令重排序:可以解决单例双重检查对象初始化代码执行乱序问题。 * volatile可以看做是轻量版的synchronized,volatile不保证原子性,但是如果是对一个共享变量进行多个线程的赋值,而没有其他的操作,那么就可以用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile又保证了可见性,所以就可以保证线程安全了。 ## 总结 ## 1. volatile修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如boolean flag;或者作为触发器,实现轻量级同步。 2. volatile属性的读写操作都是无锁的,它不能替代synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。 3. volatile只能作用于属性,我们用volatile修饰属性,这样编译器就不会对这个属性做指令重排序。 4. volatile提供了可见性,任何一个线程对其的修改将立马对其他线程可见,volatile属性不会被线程缓存,始终从主存中读取。 5. volatile提供了happens-before保证,对volatile变量v的写入happens-before所有其他线程后续对v的读操作。 6. volatile 可以使纯赋值操作是原子的,如 `boolean flag = true; falg = false`。 7. volatile可以在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性。 参考链接:[https://blog.csdn.net/u012723673/article/details/80682208][https_blog.csdn.net_u012723673_article_details_80682208] [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3MDkzNDY1_size_16_color_FFFFFF_t_70]: /images/20221022/b7bca9439f384bc49bd55054089c6e10.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3MDkzNDY1_size_16_color_FFFFFF_t_70 1]: /images/20221022/2f1676363c634210a11ea274da09e1e3.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3MDkzNDY1_size_16_color_FFFFFF_t_70 2]: https://img-blog.csdnimg.cn/2021033116140374.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3MDkzNDY1,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3MDkzNDY1_size_16_color_FFFFFF_t_70 3]: https://img-blog.csdnimg.cn/20210331141720377.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3MDkzNDY1,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3MDkzNDY1_size_16_color_FFFFFF_t_70 4]: https://img-blog.csdnimg.cn/20210331141746723.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3MDkzNDY1,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3MDkzNDY1_size_16_color_FFFFFF_t_70 5]: https://img-blog.csdnimg.cn/20210331141813462.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3MDkzNDY1,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3MDkzNDY1_size_16_color_FFFFFF_t_70 6]: https://img-blog.csdnimg.cn/20210401095631941.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3MDkzNDY1,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3MDkzNDY1_size_16_color_FFFFFF_t_70 7]: https://img-blog.csdnimg.cn/20210401104205789.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI3MDkzNDY1,size_16,color_FFFFFF,t_70 [https_blog.csdn.net_u012723673_article_details_80682208]: https://blog.csdn.net/u012723673/article/details/80682208
还没有评论,来说两句吧...