共计 3233 个字符,预计需要花费 9 分钟才能阅读完成。
文章已同步至 GitHub 开源我的项目: JVM 底层原理解析
Java 内存模型
JVM 虚拟机标准中已经试图定义一种 Java 内存模型,来屏蔽掉各种硬件和操作系统的内存拜访差别,以实现让 Java 程序在各种平台下都能够达到一致性的内存拜访成果。
然而定义这样一套内存模型并非很容易,这个模型必须足够谨严,能力让 Java 的并发内存拜访操作不会有歧义。然而也必须足够宽松,这样使得虚拟机的具体实现可能有自在的施展空间来利用各种硬件的劣势。通过长时间的验证和补救,到了 JDK1.5(实现了 JSR133 标准)之后,Java 内存模型才终于成熟起来了。
主内存和工作内存
Java 内存模型规定了所有的变量都存储在 主内存
(Main Memory) 中,每条线程都有本人的 工作内存
(Work Memory)
- 工作内存中保留了被该线程应用的变量的主内存正本,
- 线程对变量的读写操作必须在工作内存中进行。
- 而不能间接拜访主内存的数据。
- 不同的线程也不能相互读写对方的工作内存,线程之间的变量传递必须通过主内存传递。
主内存和工作内存的交互
Java 内存模型定义了如下八种操作 (每一种操作都是 原子的
, 不可再分
的)
lock 锁定
:作用于主内存,将一个变量标识为线程独占状态unlock: 解锁
:作用于主内存,将一个线程独占状态的变量开释read 读取
:从主内存读取数据到工作内存,便于之后的 load 操作load 载入
:把 read 读取操作从主内存中失去的变量放入工作内存的变量正本中use 应用
:将工作内存中的变量传递给执行引擎 当虚拟机遇到一个须要应用变量值的字节码时,执行此操作assign 赋值
:将执行引擎中的值赋给工作内存的变量。当虚拟机遇到一个赋值操作时,执行此操作store 存储
:将工作内存的值传递到主内存,便于之后的 write 操作write 写入
:将 store 存储操作中从工作内存中获取的变量写入到主内存中
举例:
- 如果要把一个变量从主内存拷贝到工作内存,则顺次执行 read 读取操作, load 载入操作
- 如果要把一个变量从工作内存写入到主内存,则顺次执行 store 存储操作,write 写入操作
上述的 8 种操作必须满足以下规定:
- 不容许 read 和 load、store 和 write 操作之一独自呈现。也就是说不容许一个变量从主内存读取然而工作内存不承受,也不容许工作内存发动回写申请然而主内存不承受。
- 不容许一个线程抛弃它的最近 assign 的操作,即变量在工作内存中扭转了之后必须同步到主内存中。
- 不容许一个线程无起因地(没有产生过任何 assign 操作)把数据从工作内存同步回主内存中。
- 一个新的变量只能在主内存中诞生,不容许在工作内存中间接应用一个未被初始化(load 或 assign)的变量。即就是对一个变量施行 use 和 store 操作之前,必须先执行过了 assign 和 load 操作。
- 一个变量在同一时刻只容许一条线程对其进行 lock 操作,但 lock 操作能够被同一条线程反复执行屡次,屡次执行 lock 后,只有执行雷同次数的 unlock 操作,变量才会被解锁。lock 和 unlock 必须成对呈现
- 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎应用这个变量前须要从新执行 load 或 assign 操作初始化变量的值
- 如果一个变量当时没有被 lock 操作锁定,则不容许对它执行 unlock 操作;也不容许去 unlock 一个被其余线程锁定的变量。
- 对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中(执行 store 和 write 操作)。
volatile 非凡规定
volatile 能够说是 Java 虚拟机提供的最轻量级的同步机制。然而它并不容易被正确,残缺的了解。
Java 内存模型中规定
当一个变量被定义为 volatile
之后,示意着线程工作内存有效,对此值的读写操作都会间接作用在主内存上,
因而它具备对所有线程的 立刻可见性
。
-
保障此变量对所有线程的
立刻可见性
当变量的值被批改之后,新值对于其余线程是立刻可知的。一般变量并不能做到这一点,因为一般变量的值在线程之间的传递是要进过主内存来实现的。比方当线程 A 对变量进行了回写操作,线程 B 只有在 A 回写实现之后,在对主内存操作,新值才对 B 是可见的。在 A 回写到主内存的过程中,B 读取的仍旧是旧值。
然而这并不能够推导出
基于 volatile 变量的运算在并发下是平安的
,因为在 Java 中的运算操作符并不是原子性
的。这导致了volatile 变量在并发下运算是不平安
的。通过代码验证
volatile 变量在并发下运算是不平安
首先咱们创立 20 个线程,每个线程对 volatile 变量进行 1000 次的自增操作。
/** * @作者: 写 Bug 的小杜【email@shaoxiongdu.cn】* @工夫: 2021/07/31 * @形容: 通过代码验证【volatile 变量在并发下运算是不平安】*/ public class VolatileTest { //volatile 润饰的 count private static volatile int count = 0; //count 自增办法 public static void increment(){count++;} public static void main(String[] args) { // 对 count 进行递增 1000 次操作的可运行接口 Runnable runnable = new Runnable() { @Override public void run() {System.out.println(Thread.currentThread().getName() + "线程开始对 count 进行递增操作"); for (int i = 0; i < 1000; i++) {increment(); } System.out.println(Thread.currentThread().getName() + "线程对 count 递增操作完结"); } }; // 创立 20 个线程并启动 for (int i = 0; i < 20; i++) {Thread thread = new Thread(runnable); thread.setName((i+1) + "号线程"); thread.start();} while (Thread.activeCount() > 2){ // 主线程回到就绪状态 Thread.yield();} System.out.println("所有线程完结,count =" + count); } }
如果此程序在并发下是平安的,那么 count 的值最初必定是 20*1000 = 20000;也就是说,如果运行后果为 20000,那么
volatile 变量在并发下运算是平安的
通过屡次运行程序,咱们发现,count 的值永远比 20000 小。
那么,这是为什么呢?
咱们将上方的代码进行反编译,而后剖析 increment 办法的字节码指令。
0 getstatic #2 <cn/shaoxiongdu/chapter6/VolatileTest.count : I> 3 iconst_1 4 iadd 5 putstatic #2 <cn/shaoxiongdu/chapter6/VolatileTest.count : I> 8 return
咱们能够发现,一行 count++ 代码被分为 4 行字节码文件去执行。通过对字节码的剖析,咱们发现,
当偏移量为 0 的字节码 getStatic 将 count 的值从局部变量表取到操作数栈顶的时候,
volatile
保障了此时 count 的值是正确的,然而在执行 iconst_1, iadd 这些操作的时候,其余线程曾经把 count 的值扭转了,此时,操作数栈顶的 count 为过期的数据,所以 putStatic 字节码指令就有可能将较小的值同步到主内存中。因而最终的值会比 20000 略微小。也就是说,
volatile 变量在并发下运算是不平安的
。在并发环境下,volatile 的变量只是对全副线程即时可见的,如果要进行写的操作,还是要通过加锁来解决。
文章已同步至 GitHub 开源我的项目: JVM 底层原理解析