synchronized 锁升级过程就是其优化的核心:偏向锁 -> 轻量级锁 -> 重量级锁
class Test{
private static final Object object = new Object();
public void test(){
synchronized(object) {
// do something
}
}
}
每个对象创建时都有各自的对象头,用来记录持有锁的状态(如下图所示32位JVM下)
一个对象创建时:
1.如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即后 3 位为 101,这时它的 thread、epoch、age 都为 0。
2.偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 XX:BiasedLockingStartupDelay=0 来禁用延迟。
3.如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即后 3 位为 001,这时它的 hashcode、 age 都为 0,第一次用到 hashcode 时才会赋值。
无锁------------>偏向锁
使用 synchronized 关键字锁住某个代码块的时候,当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。
当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。如果从头到尾都是一个线程在使用锁,很明显偏向锁几乎没有额外开销,性能极高。
在偏向锁状态下如果调用了对象的 hashCode,但此时偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被 撤销,而轻量级锁会在锁记录中记录 hashCode 重量级锁会在 Monitor 中记录 hashCode,所以无影响。
偏向锁---------->轻量级锁
一旦有第二个线程加入锁竞争,偏向锁转换为轻量级锁(自旋锁)。锁竞争:如果多个线程轮流获取一个锁,但是每次获取的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程获取锁的时候,发现锁已经被占用,需要等待其释放,则说明发生了锁竞争。
在轻量级锁状态上继续锁竞争,没有抢到锁的线程进行自旋操作,即在一个循环中不停判断是否可以获取锁。获取锁的操作,就是通过 CAS 操作修改对象头里的锁标志位。先比较当前锁标志位是否为释放状态,如果是,将其设置为锁定状态,比较并设置是原子性操作,这个 是 JVM 层面保证的。当前线程就算持有了锁,然后线程将当前锁的持有者信息改为自己。
假如我们获取到锁的线程操作时间很长,比如会进行复杂的计算,数据量很大的网络传输等;那么其它等待锁的线程就会进入长时间的自旋操作,这个过程是非常耗资源的。其实这时候相当于只有一个线程在有效地工作,其它的线程什么都干不了,在白白地消耗 CPU,这种现象叫做忙等 (busy-waiting)。所以如果多个线程使用独占锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么 synchronized 就是轻量级锁,允许短时间的忙等现象。这是一种择中的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。
每个线程都的栈帧都会包含一个锁记录(Lock Record)的结构,内部可以存储锁定对象的 Mark Word,让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存 入锁记录
如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下
如果 cas 失败,有两种情况
如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,会先进入自旋锁状态,最后进入重量级锁状态
如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一,相当于踢出一条null的锁记录。
当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
成功,则解锁成功
失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
轻量级锁--------->重量级锁
显然,忙等是有限度的(JVM 有一个计数器记录自旋次数,默认允许循环 10 次,可以修改)。如果锁竞争情况严重, 达到某个最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是通过 CAS 修改锁标志位,但不修改持有锁的线程 ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是上面说的忙等,即不会自旋),等待释放锁的线程去唤醒。在 JDK1.6 之前, synchronized 直接加重量级锁,很明显现在通过一系列的优化过后,性能明显得到了提升。
JVM 中,synchronized 锁只能按照偏向锁、轻量级锁、重量级锁的顺序逐渐升级(也有把这个称为锁膨胀的过程),不允许降级。
那具体如何升级呢
假设当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
这时 Thread-1 加轻量级锁失败,进入锁膨胀流程(这里我们就先忽略锁自旋)
即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
然后自己进入 Monitor 的 EntryList BLOCKED
当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程
Monitor 被翻译为监视器或管程
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针
刚开始 Monitor 中 Owner 为 null
当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一 个 Owner
在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入 EntryList BLOCKED
Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程