jvm synchronized 机制

本文主要讲解了JVM锁的原理以及锁升级的流程

对象锁

对象锁分为无锁、偏向锁、轻量级锁、重量级锁,且 JVM 的 synchronized 实现可根据场景依次进行锁升级。

Lock Record的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 记录锁的信息
* 锁的入栈是由高位到低位
*/
class BasicObjectLock {
friend class VMStructs;
private:
BasicLock _lock; // 这个就是我们所说的 Displaced Mark Word
oop _obj; // 锁的对象
};

class BasicLock {
private:
volatile markOop _displaced_header; // 真实被替换的 Mark Word
};

monitor的实现:

1
2
3
4
5
6
7
8
9
10
ObjectMonitor::ObjectMonitor() {  
_recursions = 0; // 线程的重入次数
_object = NULL; // 锁的对象
_owner = NULL; // 标识拥有该monitor的线程
_WaitSet = NULL; // 等待队列(wait队列)
_cxq = NULL ; // 多线程竞争锁进入时的单向链表
_EntryList = NULL; // 唤醒队列(notify队列)

....
}

1 偏向锁

1.1 偏向锁流程

1)线程1获取锁,通过 CAS 将对象的 MarkWord 替换为线程1的 id,并在线程栈中添加一条 DisplacedMarkWord 为 null 的 LockRecord。

2)线程1再次获取锁的时候,CAS 失败,此时通过 线程id 判断出是同一个线程,则再次添加一条 DisplacedMarkWord 为 null 的 LockRecord。

3)线程1退出一次锁,从最低位开始循环找到该对象的一个 LockRecord 并删除。

4)线程2获取锁,CAS失败。从虚拟机中遍历所有线程,判断该线程是否存活。如果已经消失,则执行偏向锁的重偏向或撤销(可重偏向替换为匿名偏向锁模式,否则替换为无锁模式执行后续的锁升级)。

5)如果线程1存在,==从低位到高位==遍历线程的 LockRecord。获取最高位的 LockRecord(此时该锁为最早加锁的那一个)。

6)LockRecord 不存在,执行偏向锁的重偏向或撤销。LockRecord 存在,则将该 LockRecord 的 DisplacedMarkWord 替换为对象的 MarkWord 并升级为轻量级锁。

源码:biasedLocking

1.2 批量重偏向和批量锁撤销

批量重偏向:当一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,会导偏向锁重偏向的操作。默认偏向锁批量重偏向阈值为:20(BiasedLockingBulkRebiasThreshold)

批量撤销:在多线程竞争剧烈的情况下,使用偏向锁将会降低效率,于是乎产生了批量撤销机制。默认偏向锁批量撤销阈值:40(BiasedLockingBulkRevokeThreshold)

重偏向只会发生在批量重偏向中,且在一个计数周期只会重偏向一次。

1.3 偏向锁注意事项

1)对象存在两种 MarkWord,一个是匿名偏向锁状态(101)和一个无锁状态(001)。前面的 29bit 都为 0。偏向锁在没有偏向所有者的时候,是没有线程 id 的,并且后续所有和偏向锁有关的 cas 都是将 0 替换为自己的线程id 和 Epoch。无锁状态则是因为对象的 hashcode 只有再初次调用的时候才会生成并存储在对象头。

2)开启偏向锁之后对象新建的 MarkWord 都为匿名偏向锁状态。无锁状态加锁会直接膨胀为轻量级锁。

3)通过偏向锁加锁流程我们可以知道,偏向锁记录是没有存储对象的 hashcode,因此偏向锁和 hashcode 是不能共存的。如果对象调用了 hashcode,那么它就不会进入偏向锁而是直接升级为轻量级锁;如果一个对象处于偏向锁状态调用了 hashcode,则会在线程进入安全临界点(safe point)的时候,挂起线程并将偏向锁膨胀为轻量级锁。

4)线程只会删除线程内的锁记录,不会去清除 MarkWord 里面的 线程id。即等其他线程 CAS 失败之后再决定是锁撤销再膨胀还是锁重偏向。

2 轻量级锁

2.1 轻量级流程

1)线程1获取锁,将线程的 MarkWord 复制一份保存在 LockRecord 中,通过 CAS 替换线程的 MarkWord 为该 LockRecord 的指针。

2)线程1再次获取锁的时候,CAS 失败,此时通过 线程id 判断出是同一个线程,则再次添加一条 DisplacedMarkWord 为 null 的 LockRecord。

3)线程1退出一次锁,从最低位开始循环找到该对象的一个 LockRecord 并删除。如果 LockRecord 的 DisplacedMarkWord 为 null,则代表重入锁,直接退出;如果不为 null,则代表没有再持有锁,需要通过 CAS 将对象的 MarkWrod 重新再替换回去。

4)线程2获取锁,CAS失败。根据虚拟机参数来决定是否自旋还是膨胀为重量级锁。

5) 线程1退出锁的时候,CAS 可能失败,因为可能线程2正在将锁膨胀为重量级锁,因此线程1也会进入锁膨胀的方法获取重量级锁。

源码:synchronizer

2.2 轻量级锁膨胀

偏向锁由于 LockRecord 是相同的,所以膨胀方法较简单,只要 copy MarkWord 到 DisplacedMarkWord 中就行,而轻量级锁和重量级锁是两种实现方式。重量级锁由操作系统的 MutexLock(互斥锁) 来实现的,因此操作稍微有点复杂。

  1. 调用 omAlloc 分配一个 ObjectMonitor 对象(以下简称monitor),在 omAlloc 方法中会先从线程私有的 monitor 集合 omFreeLis t中分配对象,如果 omFreeList 中已经没有 monitor 对象,则从 JVM 全局的 gFreeList 中分配一批 monitor 到 omFreeList 中。

  2. 初始化 monitor 对象。

  3. 将状态设置为膨胀中(INFLATING)状态。

  4. 设置 monitor 的 header 字段为 DisplacedMarkWord,owner 字段为 LockRecord,obj 字段为锁对象。

  5. 设置锁对象头的 markWord 为重量级锁状态,指向第一步分配的monitor对象。

3 重量级锁

简单略