使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以 使用轻量级锁来优化。
1.创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word
2.让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
3. 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下
4.如果 cas 失败,有两种情况
a.如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
b.如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重 入计数减一
当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
a.成功,则解锁成功
b.失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有 竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块
}
}
当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
即为Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
然后自己进入 Monitor 的 EntryList BLOCKED
当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁 流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程
重量级锁:每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
static final Object obj = new Object();
public static void m1() {
synchronized( obj ) {
// 同步块 A
m2();
}
}
public static void m2() {
synchronized( obj ) {
// 同步块 B
m3();
}
}
public static void m3() {
synchronized( obj ) {
// 同步块 C
}
}
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现 这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。如下图所示。(偏向第一个获得锁的线程,并把这个线程存储在对象头里,之后只要是该线程来获取锁,都能快速批准)
栈帧中包含锁记录(Lock Record:lock record 地址;Object reference)
先创建锁记录,Object reference指向锁对象(obect),并通过cas替换lock record地址和objec中的Mark Word(锁记录地址和状态);
如果失败,可能是其他线程持有了该object轻量级锁,则进入锁膨胀;也可能是自己重入,则再添加一条锁记录用来计数,同时该锁记录object reference指向object,lock record为null.
当退出synchronized时,如果发现有null的记录,表明有重入,然后重置锁记录;若不为null,则使用cas还原(Mark word恢复给Object,lock record地址恢复给lock record);如果失败,则表明进入了锁膨胀环节,则进入重量锁解锁过程。
当Thread1尝试进行轻量级锁,无法cas成功,说明有其他线程(Thread0)给该Object对象上锁,此时轻量级锁升级成重量级锁;为Object对象申请Monitor锁,Object指向该Monitor,然后Thread1会进入Monitor的EntryList中阻塞,而Monitor中的Owner会指向Thread0中lock recotd中Object reference。当Thread0退出Synchronized代码块时,发现使用cas无法解锁成功,Thread0会依据Monitor地址找到Monitor对象,设置Owner为null,然后唤醒EntryList中的Thread1
只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS,因此只需要检查该线程ID是否是自己
摘自哔哩哔哩黑马程序员笔记