Java volatile关键字原理解剖

文章目录

  • Java volatile关键字原理解剖
    • 参考文章
    • 前置知识
      • CPU缓存模型
      • CPU缓存行
      • 并发编程基本概念
      • Java锁概念
    • volatile关键字原理(主题)
      • volatile特性
      • volatile原理

参考文章

文章内容参考以下博客,并对其中volatile关键原理进行提炼,从各方面出发解剖volatile关键字。

[1] https://mp.weixin.qq.com/s/bm3VVYp_r2vWLiIUFpC-4g(Java技术迷公众号)

[2] (2条消息) Java volatile关键字最全总结:原理剖析与实例讲解(简单易懂)_老鼠只爱大米的博客-CSDN博客_java volatile

前置知识

CPU缓存模型

因为内存和CPU之间存在速度差异,CPU使用三级高速缓存来平衡内存和CPU之前的速度差。L1,L2,L3 高速缓存集成到 CPU,L0 也就是寄存器,寄存器离 CPU 最近,访问速度也最快,基本没有时延。

大家可以打开window的任务管理器,点击性能可以查看到CPU的情况,从下面的图中我们可以得知,我这个CPU有8个物理处理器(8核CPU),16个逻辑处理器(8核16线程)。还能分析高速缓存L1,L2,L3分别为512KB,4.0MB,8.0MB。

这里需要介绍一下CPU的物理核心和逻辑核心分别代表什么,CPU的物理核心代表一个CPU的处理单元的个数,比如: CPU 包含 4 个物理核心 8 个逻辑核心(4核8线程)。4 个物理核心表示在同一时间可以允许 4 个线程并行执行。而逻辑核心代表的是:处理器利用超线程的技术将一个物理核心模拟出了两个逻辑核心。

一个物理核心在同一时间只会执行一个线程,而超线程芯片可以做到线程之间快速切换,当一个线程在访问内存的空隙,超线程芯片可以马上切换去执行另外一个线程。因为切换速度非常快,所以在效果上看到是 8 个线程在同时执行。(引用文章【1】)

CPU缓存模型如下图:

可以看到L3是多核共用的,而L2,L1是属于CPU核心独立占有的。我们可以在Linux系统中在/sys/devices/system/cpu/目录下看多CPU设备的描述信息。该目录下有多少个cpux就代表有多少个逻辑核心。

假设我们进入第一个逻辑核心:/sys/devices/system/cpu/cpu0/cache,会发现一下目录:

  • index0 描述L1Cache中DataCache 的信息
  • index1 描述L1Cache 中 Instruction Cache 的信息
  • index2 描述L2Cache 的信息
  • index3 描述L3Cache 的信息

进入每个index目录,每个目录都会有以下部分或者全部的文件,分别为:

  • level:表示该 cache 信息属于哪一级,1 表示 L1Cache,以其类推
  • type:表示属于 L1Cache 的 DataCache;
  • size:表示 DataCache 的大小为 32K;
  • shared_cpu_list:之前我们提到 L1Cache 和 L2Cache 是 CPU 物理核所私有的,而由物理核模拟出来的逻辑核是共享 L1Cache 和 L2Cache 的,/sys/devices/system/cpu/ 目录下描述的信息是逻辑核。shared_cpu_list 描述的正是哪些逻辑核共享这个物理核。
  • coherency_line_size:该cache块使用的缓存行大小

CPU缓存行

CPU 的高速缓存结构,引入高速缓存的目的在于消除 CPU 与内存之间的速度差距。数据在 CPU 高速缓存中的存取并不是以单独的变量或者单独的指针为单位存取的。而是以缓存行为存取单位。

CPU 高速缓存中存取数据的基本单位叫做缓存行 cache line。缓存行存取字节的大小为 2 的倍数,在不同的机器上,缓存行的大小范围在 32 字节到 128 字节之间。目前所有主流的处理器中缓存行的大小均为 64 字节

一般现在的计算机CPU基本都是64字节为大小的存储行,这也就意味着每次 CPU 从内存中获取数据或者写入数据的大小为 64 个字节,即使你只读一个 bit,CPU 也会从内存中加载 64 字节数据进来。

比如你访问一个 long 型数组,当 CPU 去加载数组中第一个元素时也会同时将后边的 7 个元素一起加载进缓存中。这样一来就加快了遍历数组的效率

long 类型在 Java 中占用 8 个字节,一个缓存行可以存放 8 个 long 型变量。

事实上,你可以非常快速的遍历在连续的内存块中分配的任意数据结构,如果你的数据结构中的项在内存中不是彼此相邻的(比如链表),这样就无法利用 CPU 缓存的优势。由于数据在内存中不是连续存放的,所以在这些数据结构中的每一个项都可能会出现缓存行未命中(程序局部性原理)的情况。

Netty 利用数组实现的自定义 SelectedSelectionKeySet 类型替换掉了 JDK 利用 HashSet 类型实现的 sun.nio.ch.SelectorImpl#selectedKeys。目的就是利用 CPU 缓存的优势来提高 IO 活跃的 SelectionKeys 集合的遍历性能。(引用文章【1】)

并发编程基本概念

并发编程三大基本概念:

  • 原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
  • 可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  • 有序性:即程序执行的顺序按照代码的先后顺序执行。

原子性: jave关于并发编程原子性的包有:java.concurrent.Atomic.* 包,该包下的一切方法都是符合原子性的,如何描述原子性,一个很经典的例子就是银行账户转账问题。假设:从账户A向账户B转1000元,那么比如会切分为两个操作,分别是:(1).从账户A减去1000元 (2).往账户B加上1000元,如果要保证原子性的话,那么这两个操作要么全部成功,要么全部失败,不可以存在(1)成功(2)失败,反之也不可。

可见性:Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。当然,synchronize和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。(引用文章【2】)

有序性:在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序。Java内存模型中的有序性可以总结为:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象。(引用文章【2】)

Java锁概念

这里直接给我另一篇博客的链接,该博客详细地介绍了锁的概念和,Java中锁的实现的介绍。(2条消息) JAVA锁_鸭梨的药丸哥的博客-CSDN博客

volatile关键字原理(主题)

volatile特性

在前面介绍过CPU的缓存模型,CPU的缓存行和并发编程基础概念。下面的讲述的volatile关键字原理与这三者的关系十分密切。

volatile拥有以下特性:

  • 保证可见性:volatile变量会把该线程本地内存中的变量强制刷新到主内存中去,并且会让其他线程中的volatile变量缓存无效。
  • 禁止CPU指令重排:阻止CPU指令重排,确保程序按照代码的先后顺序执行

volatile原理

以下内容参考或直接引用(引用文章【1】)

假设我们现在定义一个类FalseSharding,FalseSharding代码如下,字段 a,b 之间逻辑上是独立的,它们之间一点关系也没有,分别用来存储不同的数据,数据之间也没有关联。

public class FalseSharding {volatile long a;volatile long b;
}

根据CPU缓存行介绍,我们可以得知我们一般CPU的缓存行大小为64字节,而字段 a,b 总共16字节。所以字段 a,b 有可能同时存在一个缓存行中。

如果恰好字段a,b 被 CPU 读进了同一个缓存行,而此时有两个线程,线程a用来修改字段a,同时线程b用来读取字段 b。那么就会出现下面这种情况:

为了解决缓存不一致性问题,volatile使用以下2种解决方法:

  • 通过在总线加LOCK锁的方式(Lock前缀指令)
  • 通过缓存一致性协议(Intel 的MESI协议)

Lock前缀指令和缓存一致性协议介绍如下:

  • Lock 前缀指令可以使修改线程所在的处理器中的相应缓存行数据被修改后立马刷新回内存中,并同时锁定所有处理器核心中缓存了该修改变量的缓存行,防止多个处理器核心并发修改同一缓存行;
  • 缓存一致性协议主要是用来维护多个处理器核心之间的 CPU 缓存一致性以及与内存数据的一致性。每个处理器会在总线上嗅探其他处理器准备写入的内存地址,如果这个内存地址在自己的处理器中被缓存的话,就会将自己处理器中对应的缓存行置为无效,下次需要读取的该缓存行中的数据的时候,就需要访问内存获取。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lyyHxeoJ-1649253305976)(F:\笔记文档\笔记图片\4.jpg)]

根据Lock前缀指令和缓存一致性协议,在volatile标识的数据可能会出现以下这两者情况:

第一种情况

  • 当线程 a 在处理器 core0 中对字段 a 进行修改时,Lock 前缀指令会将所有处理器中缓存了字段 a 的对应缓存行进行锁定,这样就会导致线程 b 在处理器 core1 中无法读取和修改自己缓存行的字段 b;
  • 处理器 core0 将修改后的字段 a 所在的缓存行刷新回内存中。

第二种情况

  • 当处理器 core0 将字段 a 所在的缓存行刷新回内存的时候,处理器 core1 会在总线上嗅探到字段 a 的内存地址正在被其他处理器修改,所以将自己的缓存行置为失效。
  • 当线程 b 在处理器 core1 中读取字段b的值时,发现缓存行已被置为失效,core1 需要重新从内存中读取字段 b 的值即使字段b没有发生任何变化。

0 将字段 a 所在的缓存行刷新回内存的时候,处理器 core1 会在总线上嗅探到字段 a 的内存地址正在被其他处理器修改,所以将自己的缓存行置为失效。

  • 当线程 b 在处理器 core1 中读取字段b的值时,发现缓存行已被置为失效,core1 需要重新从内存中读取字段 b 的值即使字段b没有发生任何变化。

Java volatile关键字原理解剖相关推荐

  1. [Java并发编程(三)] Java volatile 关键字介绍

    [Java并发编程(三)] Java volatile 关键字介绍 摘要 Java volatile 关键字是用来标记 Java 变量,并表示变量 "存储于主内存中" .更准确的说 ...

  2. Java Volatile关键字可见性问题分析

    Java Volatile关键字可见性问题分析 Java 内容模型 普通变量(非Vola变量)的内存不可见性 Volatile变量的内存可见性 剩余疑惑 Java 内容模型 具体可以查看这篇文章Jav ...

  3. Java volatile关键字详解

    1.关于volatile volatile是Java语言中的关键字,用来修饰会被多线程访问的共享变量,是JVM提供的轻量级的同步机制,相比同步代码块或者重入锁有更好的性能.它主要有两重语义,一是保证多 ...

  4. volite java_如何理解 JAVA volatile 关键字

    最近在重新梳理多线程,同步相关的知识点.关于 volatile 关键字阅读了好多博客文章,发现质量高适合小白的不多,最终找到一篇英文的非常通俗易懂.所以学习过程中顺手翻译下来,一方面巩固知识,一方面希 ...

  5. Java volatile关键字总结

    JMM(为什么需要volatile) Java内存模型(JavaMemoryModel)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量,存储到内存和从内存中读取变量这样的 ...

  6. java volatile关键字的作用_java volatile关键字作用及使用场景详解

    1. volatile关键字的作用:保证了变量的可见性(visibility).被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见,避免出现脏读的现象.如以下代码片段,isShut ...

  7. volatile关键字原理

    1.Volatile关键字的作用 可见性(visibility) 有序性(禁止指令重排) 不具备原子性 2.Volatile可见性实现原理 volatile是采用"内存屏障"来实现 ...

  8. java volatile关键字使用

    1.为什么要使用volatile关键字? 先看下面的代码: //线程1 boolean stop = false; while(!stop){doSomething(); }//线程2 stop = ...

  9. 一个具体的例子学习Java volatile关键字

    相信大多数Java程序员都学习过volatile这个关键字的用法.百度百科上对volatile的定义: volatile是一个类型修饰符(type specifier),被设计用来修饰被不同线程访问和 ...

最新文章

  1. celery源码分析-worker初始化分析(下)
  2. golang 绘图库_golang在图片上绘制中文不乱码的方法
  3. R语言as.Date函数将字符串转化为日期格式实战
  4. JDBC(与Orcale的连接)(转)
  5. JavaScript 触发click事件 兼容FireFox,IE 和 Chrome
  6. 力扣172.阶乘后的零
  7. linux gvim 列编辑,Linux——vim编辑器
  8. 如何选购一款好的人事档案管理系统
  9. 极客大学架构师训练营 JVM虚拟机原理 JVM垃圾回收原理 Java编程优化 秒杀 第九次作业
  10. python 服务发现_什么是服务发现?
  11. 成功的背后!(给所有IT人)
  12. 根据银行账号判定所属银行
  13. zend studio php 运行,Zend Studio使用教程:使用PHP 7进行开发(一)
  14. SpringBoot 启动时自动执行代码的几种方式
  15. Ubuntu系统界面启动后启动开机自启动后锁屏
  16. 【grpc02】安装protobuf和protoc
  17. python组合求和-近似值
  18. tauri+vue开发小巧的跨OS桌面应用-股票体检
  19. java,制作简易画图板
  20. su与su - 的区别

热门文章

  1. Altium_Designer-PCB的覆铜步骤
  2. Atitit.技术管理者要不要自己做开发??
  3. 浅谈html5 响应式布局
  4. 遭遇DBD::mysql::dr::imp_data_size unexpectedly
  5. myisam表锁及锁粒度调节
  6. Sql日期时间格式转换
  7. IDEA生成toString方法的快捷键
  8. StreamSets数据操作平台(数据移动及数据清洗强大工具)-第二篇
  9. 解决Nginx出现403 forbidden (13: Permission denied)报错的四种方法
  10. 第五章 常用Lua开发库3-模板渲染