(4) Monitor

Java对象头

以 32 位虚拟机为例,一个Java对象头包含Mark Word和Klass Word,其中Klass word 用来指示对象的类型,

而 Mark Word:

Monitor原理

Monitor 被翻译为监视器或管程 每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针 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 状态的线程

注意:

  • 一个mark word对应一个monitor
  • synchronized 必须是进入同一个对象的 monitor 才有上述的效果
  • 不加 synchronized 的对象不会关联监视器,不遵从以上规则

synchronized锁升级原理

1.轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。 轻量级锁对使用者是透明的,即语法仍然是 synchronized 假设有两个方法同步块,利用同一个对象加锁


static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}

2.锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有 竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

重量级锁自旋优化

t1线程在获取重量级锁失败时不会直接进入阻塞队列(因为阻塞和唤醒都是需要上下文切换的),线程会进行自旋几次尝试获取锁,如果在此期间原来的线程t0释放了锁,那么t1就可以获取到,反之失败则进入entryList阻塞队列。

3.偏向锁

同一个线程再获取同一把锁的时候进行的优化,线程第一次加锁的时候不是使用cas加锁,而是存入threadId,如果下一次还是同一个线程获取锁就可以直接进入同步代码块,如果不是则会进行锁升级的过程。

对象头格式说明

|--------------------------------------------------------------------|--------------------|
|                 Mark Word (64 bits)                                |        State       |
|--------------------------------------------------------------------|--------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01    |        Normal      |  //正常状态
|--------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1     | 01    |        Biased      |  //偏向锁状态
|--------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62                                      | 00    | Lightweight Locked |  //轻量级锁
|--------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62                              | 10    | Heavyweight Locked |  //重量级锁
|--------------------------------------------------------------------|--------------------|
|                                                            | 11    |   Marked for GC    |
|--------------------------------------------------------------------|--------------------|

回忆一下对象头格式 一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的thread、epoch、age 都为 0
  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值

撤销-调用hashcode

调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销

  • 轻量级锁会在锁记录中记录 hashCode
  • 重量级锁会在 Monitor 中记录 hashCode

批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID 撤销偏向锁的阈值为:20

批量撤销

当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象 都会变为不可偏向的,新建的对象也是不可偏向的

end

评论