文章目录
- 锁的类型
- 轻量级锁
- 重量级锁
- 自旋优化
- 偏向锁
- 偏向锁的细节
- 偏向锁的撤销
- 批量重偏向
- 批量撤销
- 锁消除
锁的类型
重量级锁、轻量级锁、偏向锁。
加锁过程:偏向->轻量级->重量级
轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized,锁的升级什么的不用我们考虑。
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
每一个线程的栈帧中都会包含一个锁记录结构,记录对象头中的markword。
锁记录中Object reference指向锁对象,并尝试用 cas 交换 Object 的 Mark Word,将Mark Word的值存入锁记录中。
若cas成功,对象头中存储锁地址和锁状态00(轻量级),表示该线程给此对象加锁。
如果 cas 失败,有两种情况:
- 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程。
- 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数。
当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一。
当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
- 成功,则解锁成功。
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。
重量级锁
如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时会进行锁膨胀,将轻量级锁膨胀为重量级锁。
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块
}
}
当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁。
这时Thread-1就会CAS失败,进入锁膨胀过程。
- Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
- 然后自己进入 Monitor 的 EntryList BLOCKED
- Object 对象头锁标记变为10(重量级)
当 Thread-0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程。
简单来说,我们可以简化一下:
- 就是没竞争时,自己正常加锁运行解锁即可,同一线程有锁的重入,防止没必要的加锁解锁过程。
- 如果有竞争时,新线程进入阻塞队列等待,当解锁时,唤醒阻塞队列中的线程。
自旋优化
重量级锁竞争的时候,可以使用自旋(不断的循环尝试获取重量级锁)来进行优化,如果当前线程自旋成功(当前持有锁的线程释放了锁),这时当前线程就可以避免阻塞。(阻塞再恢复,会进行上下文切换,耗费性能)
但是自旋也是会消耗性能的,尤其是进程非常多的时候,一堆进程在不断循环等待锁被释放,优点就是及时,一释放锁就能感应到。
偏向锁
在锁重入时,会产生锁记录,每次都需要尝试CAS,虽然会失败,但是CAS这个操作是没必要的。
优化:
将对象头中记录持有该锁的线程id,如果发生锁的重入,检测对象头,若是自己的,那么就不用再CAS了。
对比:
偏向锁的细节
一个对象创建时:
-
如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 thread、epoch、age 都为 0。
-
偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟。
-
如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值。
-
正常状态对象一开始是没有 hashCode 的,第一次调用才生成。
-
调用了 hashCode() 后会撤销该对象的偏向锁。
从图中也可以看出,偏向锁的markword没有位置可以放hashcode了,除非转变为轻量级锁,所以调用对象的hashCode会撤销他的偏向锁。
偏向锁的撤销
除了上面说的调用hashCode,也可用 让其他线程使用偏向锁,这时偏向锁会升级为普通锁。
不过两个线程需要错开加锁,不然会升级为重量级锁。
批量重偏向
可用发现,就算错开没用竞争,那么锁也会从偏向锁转变为普通锁,那其实锁的转变也是消耗性能的,所以就有了偏向锁的重偏向这个功能。
如果对象被多个线程访问,但没有竞争(错开),这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID 为 T2线程。
当某对象(这一类的不同对象)撤销偏向锁阈值超过20次后,jvm会觉得,我是不是偏向错了,于是会在给这些对象加锁的时候重新偏向至新加锁线程。
批量撤销
当相同类型不同对象撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向,这个锁竞争太激烈了。于是整个类的所有对象都会变为不可偏向的,新建的该类型对象也是不可偏向的(轻量级)。
锁消除
简单来说,当jvm调用某方法达到一定阈值,就会用记时编译器优化,发现锁加和不加没有影响时,就会去除掉锁。
public class MyBenchmark {
static int x = 0;
public void a() throws Exception {
x++;
}
public void b() throws Exception {
Object o = new Object();
synchronized (o) {
x++;
}
}
}
比如Object为局部变量,根本不会逃离此方法作用范围,不能被共享,那加不加锁没啥意义,jvm就会去除掉锁去运行,从而优化性能。