Java Synchronize底层原理总结
作者:EzreaLwj
对象内存结构
对象头:MarkWord
存储对象头的信息,Klass Word
描述对象实例的具体类型
实例数据:成员变量
对齐填充:如果对象头 + 实例变量 不是 8 的整数倍,则通过对齐填充补齐
MarkWord 解析
hashcode
:25位的对象标识Hash码age
:对象分代年龄占4位biased_lock
:偏向锁标识,占1位,0表示没有开始偏向锁,1表示开启了偏向锁thread
:持有偏向锁的线程ID,占23位epoch
:偏向时间戳,占2位ptr_to_lock_record:
轻量级锁状态下,指向栈中锁记录的指针,占30位ptr_to_heavyweight_monitor
:重量级锁状态下,指向对象监视器Monitor的指针,占30位
LockRecord 锁记录
Markword:记录锁记录的地址
对象引用:引用被加上锁了的对象
重量级锁
Monitor
Monitor 监视器,是由 jvm 提供的,由 C++ 实现的,有三个实现部分
WaitSet
:调用了 wait 方法的线程在这里等待,处于 WAITED 状态
EntryList
:没有抢到对象锁的线程在这里等待,处于 BLOCKED 状态
Owner
:存储已经抢到锁的线程对象
Monitor
的实现属于重量级锁,涉及到 内核态和用户态的切换,线程的上下文切换,每个 Java
对象都会关联一个 Monitor
对象,如果使用 Synchronize
给该对象加锁,那么 Java
对象上面的 MarkWord
地址就被设置为指向该 Monitor
对象的指针
轻量级锁
加锁流程:
- 在线程栈中创建一个
Lock Record
对象,它的object reference
字段指向锁对象 - 通过 CAS 指令把
Lock Record
的地址存放到对象头的Markword
中,如果是无锁状态则修改成功,代表该线程获取了轻量级锁 - 如果当前线程已经持有该锁,就代表是一次锁重入,设置
Lock Record
的第一部分为null
,起到一个重入计数器的作用 - 如果
CAS
修改失败,则说明发生了竞争,需要膨胀为重量级锁
解锁过程:
- 遍历线程栈,找到所有
object reference
字段等于当前锁对象的Lock record
- 如果
Lock record
的MarkWord
为null
,代表这是一次重入,将obj
设置为null
后continue
即可 - 如果
Lock record
的Markword
不为null
,则利用CAS
指令将对象头的markword
与对象对象头的markword
进行替换,如果成功则恢复为无锁状态,如果失败则膨胀为重量级锁
Markword 记录
开始时的状态
替换后的状态
偏向锁
背景:轻量级锁在没有竞争的时候,每次重入都需要进行 CAS
操作
Java 6
中 引入偏向锁来做进一步的优化:只有第一次 操才使用 CAS
将线程 ID 设置到对象的 markword
头,之后发现这个线程 ID 是自己就不会产生竞争,不用重新 CAS
,以后只要不发生竞争,这个对象就归这个线程所有
代码示例:
public class Thread5 { private static final Object object = new Object(); public static void method1() { synchronized (object) { method2(); } } public static void method2() { synchronized (object) { method3(); } } public static void method3() { synchronized (object) { } } }
总结
Java 中的 Synchronize 有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争的情况
重量级锁:底层使用 Monitor
实现,里面涉及到了用户态和内核态的转换、进程的上下文切换,成本较高,性能比较低
轻量级锁:线程加锁时间是错开的(也就是没有竞争),可以用轻量级锁来优化,轻量级修改了对象头的锁标志,相对重量级锁性能提升了许多,每次修改都是 CAS
操作,保证原子性
偏向锁:一段很长的时间内都只被一个线程使用锁,可以使用偏向锁,第一次获得锁时,会有一个 CAS
操作,之后该线程再获取锁,只需要判断 mark word
中是否是自己的线程 id 即可,而不是开销相对较大的 CAS 命令
以上就是Java Synchronize底层原理总结的详细内容,更多关于Java Synchronize底层原理的资料请关注脚本之家其它相关文章!