Java并发编程的艺术(2)synchronized和volatile底层实现原理

volatile的定义与实现

Java编程语言允许线程访问共享变量,为了确保共享变量能够准确和一致的更新,线程应该通过排他锁单独获取这个变量。如果一个字段被声明为volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。

Lock前缀

有volatile变量修饰的共享变量进行读写操作时,JVM就会向处理器发送一条Lock前缀指令,Lock前缀的指令在多核CPU处理器下会引发以下两件事情
(1)将当前处理器缓存行的数据写回到系统内存
(2)这个写回到内存的操作会使其他CPU里缓存了该内存地址的数据无效。
为了提高处理速度,处理器不直接和内存通信,而是先将系统内存的数据读到内部缓存(L1,L2或者其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。如果其他处理器缓存的值还是旧的,再执行计算操作还是会出现问题。所以在多线程编程下,为了保证各个处理器的缓存值使一致的,就会实现缓存一致性,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是否过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作时,会重新从系统内存中把数据读到处理器缓存里。

volatile的两条实现原则

(1)Lock前缀指令会引起处理器缓存写回到内存
(2)一个处理器的缓存回写到内存时会导致其他处理器的缓存无效

synchronized的实现原理与应用

Java中的每一个对象都可以作为锁。
(1)对于普通同步方法,锁是当前实例对象
(2)对于静态同步方法,锁是当前类的Class对象
(3)对于同步方法块,锁是synchronized括号里配置的对象。
当一个线程试图访问同步代码块时,他首先必须得到锁,推出或抛出异常时必须释放锁。

Java对象头

synchronized用的锁是存在Java对象头里的。
对象头的组成 Mark Word和Class Metadata Address或者Array length

Mark Word

默认存储对象的HashCode、分代年龄和所标记位。
32位JVM的Mark Word的默认存储结构表

锁状态 25bit 4bit 1bit是否是偏向锁 2bit锁标志位
无锁状态 对象的hashCode 对象的分代年龄 0 01

运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。
Mark Word的状态变化表
在这里插入图片描述
在64位虚拟机下,Mark Word是64bit大小的,其存储结构表
在这里插入图片描述

synchronized在JVM里的实现原理

JVM基于进入和退出时Monitor对象来实现方法同步和代码块同步,但两者的实现方式不一样。代码块同步是使用monitor enter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的。
monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结 束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有 一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter 指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

自旋锁

是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

CAS算法

即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数

需要读写的内存值 V

进行比较的值 A

拟写入的新值 B

当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

发布了24 篇原创文章 · 获赞 1 · 访问量 549

猜你喜欢

转载自blog.csdn.net/qq_45366515/article/details/105116001