范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

深入理解关键字volatile

  volatile 关键字的作用是什么?
  相比于 synchronized 关键字(重量级锁)对性能影响较大,Java提供了一种较为轻量级的可见性和有序性问题的解决方案,那就是使用 volatile 关键字。由于使用 volatile 不会引起上下文的切换和调度,所以 volatile 对性能的影响较小,开销较低。
  从并发三要素的角度看,volatile 可以保证其修饰的变量的可见性和有序性,无法保证原子性(不能保证完全的原子性,只能保证单次读/写操作具有原子性,即无法保证复合操作的原子性)。
  下面将从并发三要素的角度介绍 volatile 如何做到可见和有序的。1. volatile 如何实现可见性?
  什么是可见性?
  可见性指当多个线程同时访问共享变量时,一个线程对共享变量的修改,其他线程可以立即看到(即任意线程对共享变量操作时,变量一旦改变所有线程立即可以看到)。1.1 可见性例子/**  * volatile 可见性例子  * @author 单程车票  */ public class VisibilityDemo {      // 构造共享变量     public static boolean flag = true; //    public static volatile boolean flag = true;   // 如果使用volatile修饰则可以终止循环      public static void main(String[] args) {         // 线程1更改flag         new Thread(() -> {             // 睡眠3秒确保线程2启动             try { TimeUnit.SECONDS.sleep(3);  } catch (InterruptedException e) {e.printStackTrace();}             // 修改共享变量             flag = false;             System.out.println("修改成功,当前flag为true");         }, "one").start();          // 线程2获取更新后的flag终止循环         new Thread(() -> {             while (flag) {              }             System.out.println("获取到修改后的flag,终止循环");         }, "two").start();     } }不使用 volatile 修饰 flag 变量时,运行程序会进入死循环,也就是说线程1对 flag 的修改并没有被线程2读到,也就是说这里的flag并不具备可见性。使用 volatile 修饰 flag 变量时,运行程序会终止循环,打印提示语句,说明线程2读到了线程1修改后的数据,也就是说被 volatile 修饰的变量具备可见性。1.2 volatile 如何保证可见性?
  被 volatile 修饰的共享变量 flag 被一个线程修改后,JMM(Java内存模型)会把该线程的CPU内存中的共享变量 flag 立即强制刷新回主存中,并且让其他线程的CPU内存中的共享变量 flag 缓存失效,这样当其他线程需要访问该共享变量 flag 时,就会从主存获取最新的数据。
  所以通过 volatile 修饰的变量可以保证可见性。
  两点疑问及解答:为什么会有CPU内存? 为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1/L2/其他)后再进行操作,但是操作完后的数据不知道何时才会写回主存。所以如果是普通变量(未被修饰的),什么时候被写入主存是不确定的,所以读取的可能还是旧值,因此无法保证可见性。各个线程的CPU内存是怎么保持一致性的? 实现了缓存一致性协议(MESI),MESI在硬件上约定了:每个处理器通过嗅探在总线上传播的数据来检查自己的CPU内存的值是否过期,当处理器发现自己的缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置为无效状态。当处理器对该数据进行修改操作时,会重新从系统内存(主存)中把数据读到处理器缓存(CPU内存)里。1.3 volatile 实现可见性的原理
  原理一:Lock指令(汇编指令)
  通过上面的例子的Class文件查看汇编指令时,会发现变量有无被 volatile 修饰的区别在于被 volatile 修饰的变量会多一个lock前缀的指令。
  lock前缀的指令会触发两个事件:将当前线程的处理器缓存行(CPU内存的最小存储单元,这里可以大致理解为CPU内存)的数据写回到主存(系统内存)中写回主存的操作会使其他线程的CPU内存中该内存地址的数据无效(缓存失效)
  所以使用 volatile 修饰的变量在汇编指令中会有lock前缀的指令,所以会将处理器缓存的数据写回主存中,同时使其他线程的处理器缓存的数据失效,这样其他线程需要使用数据时,会从主存中读取最新的数据,从而实现可见性。
  原理二:内存屏障(CPU指令)
  volatile的可见性实现除了依靠上述的LOCK指令(汇编指令)还依靠内存屏障(CPU指令)。
  为了性能优化,JMM 在不改变正确语义的前提下,会允许编译器和处理器对指令序列进行重排序。JMM 提供了内存屏障阻止这种重排序。
  这里介绍的是内存屏障中的一类:读写屏障(用于强制读取或刷新主存的数据,保证数据一致性)Store屏障:当一个线程修改了volatile变量的值,它会在修改后插入一个写屏障,告诉处理器在写屏障之前将所有存储在缓存中的数据同步到主内存。Load屏障:当另一个线程读取volatile变量的值,它会在读取前插入一个读屏障,告诉处理器在读屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果。
  对上面的例子使用javap查看JVM指令时,如果被 volatile 修饰时多一个 ACC_VOLATILE,JVM把字节码生成机器码时会在相应位置插入内存屏障指令,因此可以通过读写屏障实现 volatile 修饰变量的可见性。
  注意读写屏障的特点:可以将所有变量(包括不被 volatile 修饰的变量)一起全部刷入主存,尽管这个特性可以使未被 volatile 修饰的变量也具备所谓的可见性,但是不应该过于依赖这个特性,在编程时,对需要要求可见性的变量应当明确的用 volatile 修饰(当然除了volatile,synchronized、final以及各种锁都可以实现可见性,这里不过多说明)。2. volatile 如何实现有序性?
  有序性是什么?
  有序性指禁止指令重排序,即保证程序执行代码的顺序与编写程序的顺序一致(程序执行顺序按照代码的先后顺序执行)。
  为什么会发生指令重排序?
  现代计算机为了能让指令的执行尽可能的同时运行起来,采用指令流水线的方式,若指令之间不具有依赖,可以使流水线的并行最大化,所以CPU对无依赖的指令可以乱序执行,这样可以提高流水线的运行效率,在不影响最后结果的情况下,Java编译器可以通过指令重排序来优化性能。
  编译器和处理器常常会对指令做重排序,一般分为三种类型:编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。指令级并行重排序:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。内存系统重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
  所以指令重排序是指编译器和处理器为了优化程序的性能,在不改变数据依赖性的情况下,调整指令的执行顺序。
  这种优化在单线程情况下没有问题,但是在多线程情况下可能会导致影响程序结果。接下来将介绍一个多线程下指令重排的例子。2.1 有序性例子
  这里以单例模式的常用实现方式 DLC双重检查 为例子/**  * volatile 有序性例子  * @author 单程车票  */ public class Singleton {          // 使用volatile进行修饰     private static volatile Singleton instance;      // 私有化构造器     private Singleton() {}          // 双重检查锁     public static Singleton getInstance() {         if (instance == null){             synchronized (Singleton.class){                 if (instance == null){                     instance = new Singleton();                 }             }         }         return instance;     } }
  如果写过单例模式的双重锁检查实现方式,会发现声明的变量被volatile修饰,那么为什么这里需要使用volatile修饰呢?
  第一个原因是可见性,如果没有 volatile 修饰的话,当一个线程给 instance 赋值即instance = new Singleton();后,其他线程如果无法及时看到 instance 更新,会导致创建多个单例对象,这样就不符合单例模式设计思想了,所以需要使用 volatile 修饰。
  第二个原因则是禁止指令重排序(保证有序性),为什么需要禁止指令重排呢?
  首先需要了解实例一个对象可以分为三个步骤:分配内存空间初始化对象将对象引用赋值给变量
  由于指令可以进行重排序,所以步骤可能发生变化变为分配内存空间将对象引用赋值给变量初始化对象
  如果未使用 volatile 修饰变量的话,多线程情况下可能出现这样的情况:
  一个线程在执行第二步(将对象引用赋值给变量,即此时变量不为 null )时,而另一个线程进入第一次非空检查,此时发现变量不为 null ,直接返回对象,但是此时的对象由于指令重排序的原因并未进行初始化,即返回了一个未初始化的对象。将一个未初始化的变量暴露出来会导致不可预料的后果。
  所以需要 volatile 保证变量有序性,禁止指令重排序。2.2 volatile 实现有序性的原理
  内存屏障的四种指令
  内存屏障中禁止指令重排序的内存屏障的四种指令
  指令
  说明
  LoadLoad 屏障
  保证在该屏障之后的读操作,不会被重排序到该屏障之前的读操作
  StoreStore屏障
  保证在该屏障之后的写操作,不会被重排序到该屏障之前的写操作,并且该屏障之前的写操作已被刷入主存
  StoreLoad 屏障
  保证在该屏障之后的读操作,能够看到该屏障之前的写操作对应变量的最新值
  LoadStore 屏障
  保证在该屏障之后的写操作,不会被重排序到该屏障之前的读操作
  Java编译器会在生成指令时在适当位置插入内存屏障来禁止特定类型的处理器重排序。
  volatile的插入屏障策略在每个 volatile 写操作的前面插入一个 StoreStore 屏障在每个 volatile 写操作的后面插入一个 StoreLoad 屏障在每个 volatile 读操作的后面插入一个 LoadLoad 屏障在每个 volatile 读操作的后面插入一个 LoadStore 屏障
  即在每个volatile写操作前后分别插入内存屏障,在每个volatile读操作后插入两个内存屏障。
  如何通过内存屏障保持有序性?
  分析上面的双重检查锁例子:
  不加 volatile 修饰时,多线程下可能出现的情况是这样的:
  为了避免这种情况,使用 volatile 修饰变量时,会插入内存屏障// 双重检查锁 public static Singleton getInstance() {     if (instance == null){                   // 第一次检查         synchronized (Singleton.class){      // 加锁             if (instance == null){           // 第二次检查                 插入 StorStore屏障           // 插入屏障禁止下面的new操作和读取操作重排序                 instance = new Singleton();  // 创建对象                 插入 LoadLoad屏障            // 插入屏障禁止下面的读取操作和上面的new操作重排序             }         }     }     return instance; }
  这里使用 volatile 修饰变量并不能避免实例对象的三个步骤重排序,因为 volatile 关键只能避免多个线程之间的重排序,不能避免单个线程内部的重排序。
  这里 volatile 保证有序性的作用在于插入屏障之后必须等创建对象完成后才能进行读取操作,也就是说需要线程1的创建对象整个步骤完成后才会让线程2进行读取,禁止了重排序,这样就避免了返回一个未初始化的对象,保证了有序性。3. volatile 为什么不能保证原子性?
  什么是原子性?
  原子性指一个操作或一系列操作是不可分割的,要么全部执行成功,要么全部不执行(中途不可被中断)。
  为什么volatile不能保证原子性呢?
  通过一个例子来证明volatile不能保证原子性/**  * 原子性例子  * @author 单程车票  */ public class AtomicityDemo {          // 使用volatile修饰变量     public static volatile int i = 0;      public static void main(String[] args) {         ExecutorService pool = Executors.newFixedThreadPool(1000);                  // 多线程情况下执行1000次         for (int j = 0; j < 1000; j++) {             pool.execute(() -> i++);         }                  // 打印结果         System.out.println(i);         pool.shutdown();     } }  /* 输出结果:     997 */
  正常情况下,打印结果应该为1000,但是这里却是997,说明这段程序并不是线程安全的,可以看出 volatile 无法保证原子性。
  准确来说应该是 volatile 无法保证复合操作的原子性,但能保证单个操作的原子性。
  这里 volatile 保证单个操作的原子性可以应用于 使用 volatile 修饰共享的 long 或者 double 变量(可以避免字分裂情况,具体想要了解到可以查阅相关资料这里不做过多说明)。
  i++ 操作是原子操作吗?
  i++ 其实不是原子操作,实际上 i++ 分为三个步骤:读取 i 的值将 i 自增1(i + 1)写回 i 的新值(i = i + 1)
  这三个步骤每一步都是原子操作,但是组合起来就不是原子操作了,在多线程情况下同时执行 i++,会出现数据不一致性的问题。
  所以可以证明 volatile 修饰的变量无法保证原子性。
  可以通过 AtomicInteger 或者 synchronized 来保证 i++ 的原子性。4. volatile 常见的应用场景?4.1 状态标志位
  使用 volatile 修饰一个变量通过赋值不同的常数或值来标识不同的状态。/**  * 可以通过布尔值来控制线程的启动和停止  */ public class MyThread extends Thread {          // 状态标志变量     private volatile boolean flag = true;          // 根据状态标志位来执行     public void run() {         while (flag) {             // do something         }     }     // 根据状态标志位来停止     public void stopThread() {         flag = false; // 改变状态标志变量     } }4.2 双重检查DLC
  在多线程编程下,一个对象可能会被多个线程同时访问和修改,而且这个对象可能会被重新创建或者赋值为另一个对象。此时可以通过 volatile 来修饰该变量,保证该变量的可见性和有序性。
  就如单例模式的双重检查DLC可以通过 volatile 来修饰从存储单例模式对象的变量。/**  * 单例模式的双重检查方式  */ public class Singleton {          // 使用volatile进行修饰     private static volatile Singleton instance;      // 私有化构造器     private Singleton() {}          // 双重检查锁     public static Singleton getInstance() {         if (instance == null){             synchronized (Singleton.class){                 if (instance == null){                     instance = new Singleton();                 }             }         }         return instance;     } }4.3 较低开销的读写锁
  使用 volatile 结合 synchronized 实现较低开销的读写锁,由于 volatile 可以保证变量的可见性和有序性,而 synchronized 可以保证变量的原子性和互斥性,可以结合使用实现较低开销的读写锁。/**  * 读写锁实现多线程下的计数器  */ public class VolatileSynchronizedCounter {     // volatile变量     private volatile int count = 0;     // synchronized方法     public synchronized void increment() {          count++; // 原子操作     }     public int getCount() {         return count;     } }
  使用 volatile 修饰变量,synchronized 修饰方法,这样 volatile 修饰变量具有可见性,写操作会被其他线程立刻可见,synchronized 修饰方法保证 count++ 操作的原子性和互斥性,这样实现的读写锁,读操作无锁,写操作有锁,降低了开销。

一蹬上板,这次要好好表现一把了而此番我们细化至现代单板滑雪,其发展至今已有近六十年的历史,此外漫漫长河中人们对于单板装备不断升级的更迭更是没有停止如何让滑手更加舒适的滑行怎样提升运动员们的运动水平改进哪些传统部大成拳打人大成拳打人,不是轮胳膊踢腿,也不是擒拿摔跤。以肢体的大动而发挥拿化发者,乃小乘功夫以内外如一的桩功蠕动而发挥拿化发者,才为大乘功夫。所谓大动不如小动,小动不如蛄蝾者是。大成打人者,国足最强门将李富胜正值壮年却意外离世,各界人士纷纷悼念2023年2月14日,中国足球协会主席党委副书记陈戌源被曝涉嫌严重违纪,正在接受纪委的调查。自男足主教练李铁被捕后,这已经是足坛扫黑风暴中第4位落马的高官了。从2014年起,中国男南京烟笼寒水月笼纱,六朝古都金陵城南京,提起这个城市,你能想到什么?是近代伟大的民主革命先行者孙中山先生,是那段不可磨灭的历史记忆。或许是路两旁高大的法国梧桐,好吃的咸水鸭,灌汤包。初来南京,玄武湖上氤氲的水汽,给青海大通县娘娘山冰洞如玉雕现滴水成冰之景图为冰洞内部。车淑蓉摄图为冰柱。车淑蓉摄图为挂在洞顶的冰柱。车淑蓉摄图为挂在洞顶的冰柱。车淑蓉摄图为形态各异的冰柱。车淑蓉摄图为挂在洞顶的冰柱。车淑蓉摄图为冰洞内部。车淑蓉摄日前,春意渐浓2月12日,游客在四川省泸州市江阳区丹林镇建设村赏花拍照。早春时节,人们纷纷走出家门踏青赏花,尽享春日惬意。2月12日,游客在广西柳州市龙潭公园游览(无人机照片)。2月12日,游人十堰又新增一网红打卡地!地点在这里!春日时节,遍地美好!正是出门游玩的好季节,不如去探索地下岩洞,寻觅大自然留下的奇迹,感受炫彩夺目的光影艺术,欣赏一场绝美的视觉盛宴,十堰又新增一网红打卡地天惊洞景区。天惊洞景区位于财智头条奢品大牌卷国潮风情人节珍贵还是真贵?告别闷了三年的疫情时代,今年消费市场活力重燃,作为过完农历新年之后的第一个全球性节日,这个情人节前夕,暗潮涌动。市场竞争愈发激烈,奢侈品品牌愈加寄予厚望情人节等营销节点,而随着国潮情人节高甜送礼攻略!一定送到TA的心坎里品牌官方图GIVENCHY纪梵希高定恒颜持妆气垫C105(心动限量版)纪梵希心动限量版设计灵感来自于纪梵希创意总监MatthewM。Williams所打造的牛仔夹克,展现品牌经典的辨识度极佳三星ZFold4上市,三星ZFold3命运悲惨改写低价记录我们来看看外观设计,三星GalaxyZFold4的外观很有特色,辨识度极佳,横向折叠屏幕看起来像是一台迷你版的笔记本,关上之后就像是一台普通的直板手机,一开一合给人一种别具一格的感vivo又一5G新机曝光!联发科天玑700加持外观长这样?手机中国新闻vivo手机业务主要拥有XS和Y等几条产品线,其中Y系列手机虽然定位不高,但在市场上有着不错的竞争力,因此也深受消费者喜爱。据手机中国了解,vivo很快将在印度市场推出
平坝樱花盛花期即将开启,这些小细节一定要知晓,才会不虚此行!一到春季,许多人都开始计划起了自己的旅行,那么我们的春天应该以什么样的方式开启呢?平坝樱花不如就以一场浪漫的樱花秀来开启这一场美丽的春天吧!不如就去到南方,奔赴一场浪漫的约会!平坝天地渐开,晨曦里两粒羞答答的星子陪孩子读诗陪孩子读诗计划第188期点击收听晨曦吉葡乐你睁眼看我天地渐开,晨曦里两粒羞答答的星子一头鹿逃过青草地把梅花遗忘在树丛一群斑马跨越一条清水溪将斑纹赠予溪水狂热的心在江边晃动喜悦,幸福孩子腺样体肥大不动刀,用好2个组合方就可以了想起来一个多月前,已经下班了,结果有个家长带孩子来找我。说孩子腺样体肥大,天天睡觉打呼,磨牙,爬睡而且晚上睡不着。孩子家长也是不愿意做手术,怕孩子手术中有风险,所以我就给他加了个号杀敌一千自损两千,快船老将莫里斯为什么依然能够霸占先发位置?在NBA比赛中,每个球员都渴望成为一名能够为球队贡献的核心球员,而快船队的锋线老将莫里斯就是这样一位具有这种价值的球员。他能够在球队中霸占先发位置,并为球队创造出机会。那么,究竟是重马鸣枪开跑,这群骄邻的身影格外耀眼3月19日,2023年重庆马拉松在重庆南滨公园鸣枪开跑。这是阔别3年之后,重马在南滨路的再度归来!一路上,3万人奔跑在长嘉汇大景区,用脚步去丈量这片充满希望的土地,尽情享受青春的澎大反转!男足球员齐天羽深夜被曝出轨,其妻上午道歉辟谣这世界太疯狂了,女篮李梦那边裸聊风波还未平息,刚刚,男足这边又曝出效力于山东泰山队中场球员齐天羽的暧昧聊天。最讽刺的是齐天羽和妙龄女子的聊天背景是他身穿泰山队队服与怀孕妻子挺着大肚肇庆星乡贤冼东妹将家乡优秀体育人才向上输送同学们在上好文化课的同时,每天都要抽一个小时进行体育锻炼,做到劳逸结合,希望大家都能找到自己喜欢的运动项目。日前,奥运冠军广东省柔道协会主席冼东妹来到肇庆封开县大洲镇学校,参加情系全英羽毛球公开赛半决赛中国队男女单打有惊喜2023全英羽毛球公开赛北京时间今天早上结束了半决赛的争夺,中国羽毛球队依然表现出色,进入了男女单打和混双的决赛,男单更是提前包揽了冠亚军。2023全英公开赛半决赛中国队最大的惊喜世界杯冠军!首次转自人民日报客户端3月18日,在加拿大马克汉姆举行的世界泳联花样游泳世界杯比赛中,中国队组合程文涛石浩玙以236。2292的高分夺得混双技术自选项目冠军,得分超出第二名42。587中国公民携带超额黄金饰品入境被尼泊尔方羁押?中使馆回应问近期,国内有社交媒体传播一条消息称,1名中国公民在随团入境尼泊尔时,因携带超额黄金饰品被尼有关部门抓扣羁押,缴纳高额罚款后才被释放。请问有关情况是否属实?答我们注意到有关信息。经跟随老韩走遍中国,台湾自助游之一2012年北京开始有旅行社经营台湾游的业务,终于有大陆居民能踏上宝岛台湾的一天了,迅速掀起了去台湾旅游的热潮,我也赶紧去旅行社资询,那时旅行社只有为数不多的几条线路,7十1环岛之旅