作者:京东零售 刘跃明

Monitor 概念

Java 对象的内存布局

对象除了我们自定义的一些属性外,还有其它数据,在内存中可以分为三个区域:对象头、实例数据、对齐填充,这三个区域组成起来才是一个完整的对象。

对象头:在 JVM 中需要大量存储对象,存储时为了实现一些额外的功能,需要在对象中添加一些标记字段用于增强对象功能,这些标记字段组成了对象头。

实例数据:存放类的属性数据信息,包括父类的属性信息。

对齐填充:由于虚拟机要求对象其实地址必须是 8 字节的整数倍,需要存在填充区域以满足 8 字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。





图 1

Java 对象头

JVM 中对象头的方式有以下两种(以 32 位虚拟机为例):

普通对象

Object Header (64 bits)
Mark Word (32 bits) Klass Word (32 bits)

数组对象

Object Header (96 bits)
Mark Word(32bits) Klass Word(32bits) array length(32bits)

Mark Word

这部分主要用来存储对象自身的运行数据,如 hashcode、gc 分带年龄等,Mark Word 的位长度为 JVM 的一个 Word 大小,也就是说 32 位 JVM 的 Mark Word 为 32 位,64 位 JVM 为 64 位。为了让一个字大小存储更多的信息,JVM 将字的最低两个位设置为标记位,不同标记位下的 Mark Word 示意如下:

Mark Word (32 bits) State
identity_hashcode:25 age:4 biased_lock:1 lock:2 Normal
thread:23 epoch:2 age:4 biased_lock:1 lock:2 Biased
ptr_to_lock_record:30 lock:2 LightweightLocked
ptr_to_heavyweight_monitor:30 lock:2 HeavyweightLocked
 lock:2 Marked for GC

其中各部分的含义如下:

lock: 2 位的锁状态标记位,该标记的值不同,整个 Mark Word 表示的含义不同。

biased_lock lock 状态
0 01 无锁
1 01 偏向锁
0 00 轻量级锁
0 10 重量级锁
0 11 GC 标记

biased_lock: 对象是否启用偏向锁标记,只占 1 个二进制位,为 1 时表示对象启用偏向锁,为 0 时表示对象没有偏向锁。

age: 4 位的 Java 对象年龄,在 GC 中,如果对象再 Survivor 区复制一次,年龄增加 1,当对象达到设定的阈值时,将会晋升到老年代,默认情况下,并行 GC 的年龄阈值为 15,并发 GC 的年龄阈值为 6,由于 age 只有 4 位,所以最大值为 15,这就是-XX:MaxTenuringThreshold 选项最大值为 15 的原因。

identity_hashcode: 25 位的对象表示 Hash 码,采用延迟加载技术,调用方法 System.idenHashcode() 计算,并会将结果写到该对象头中,当对象被锁定时,该值会移动到管程 Monitor 中。

thread: 持有偏向锁的线程 ID。

epoch: 偏向时间戳。

ptr_to_lock_record: 指向栈中锁记录的指针。

ptr_to_heavyweight_monitor: 指向管程 Monitor 的指针。

Klass Word

这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM 通过这个指针确定对象是哪个类的实例,该指针的位长度为 JVM 的一个字大小,即 32 位的 JVM 为 32 位,64 位的 JVM 为 64 位。

array length

如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着 JVM 架构的不同而不同:32 位的 JVM 长度为 32 位,64 位 JVM 则为 64 位。

Monitor 原理

Monitor 被翻译为监视器管程

每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针。

Monitor 结构如下:





图 2

•刚开始 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 中等待的线程来竞争锁,竞争是非公平的,也就是先进并非先获取锁

•图 2 中 WaitSet 中的 Thread-0、Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲 wait-notify 时会分析

注意:

•synchronized 必须是进入同一个对象的 Monitor 才有上述的效果

•不加 synchronized 的对象不会关联监视器,不遵从以上规则

synchronized 原理

static final Object lock = new Object();
static int counter = 0;

public static void main(String[] args) {
    synchronized (lock) {
        counter++;
    }
}

对应的字节码为:

public static main([Ljava/lang/String;) V

TRYCATCHBLOCK L0 L1 L2 null

TRYCATCHBLOCK L2 L3 L2 null

L4

LINENUMBER 6 L4

GETSTATIC MyClass03.lock : Ljava/lang/Object;

DUP

ASTORE 1

MONITORENTER //注释 1

L0

LINENUMBER 7 L0

GETSTATIC MyClass03.counter : I

ICONST_1

IADD

PUTSTATIC MyClass03.counter : I

L5

LINENUMBER 8 L5

ALOAD 1

MONITOREXIT //注释 2

L1

GOTO L6

L2

FRAME FULL [[Ljava/lang/String; java/lang/Object] [java/lang/Throwable]

ASTORE 2

ALOAD 1

MONITOREXIT //注释 3

L3

ALOAD 2

ATHROW

L6

LINENUMBER 9 L6

FRAME CHOP 1

RETURN

L7

LOCALVARIABLE args [Ljava/lang/String; L4 L7 0

MAXSTACK = 2

MAXLOCALS = 3

注释 1

MONITORENTER 的意思为:每个对象都有一个监视锁(Monitor),当 Monitor 被占用时就会处于锁定状态,线程执行 MONITORENTER 指令时尝试获取 Monitor 的所有权,过程如下:

•如果 Monitor 的进入数为 0,则该线程进入 Monitor,并将进入数设置为 1,该线程即为 Monitor 的所有者(Owner)

•如果该线程已经占用 Monitor,只是重新进入 Monitor,则进入 Monitor 的进入数加 1

•如果其它线程已经占用 Monitor,则该线程进入阻塞状态,直到 Monitor 进入数为 0,再重新尝试获取 Monitor 的所有权

注释 2

MONITOREXIT 的意思为:执行指令时,Monitor 的进入数减 1,如果减 1 后进入数为 0,该线程退出 Monitor,不再是这个 Monitor 的所有者,其它被 Monitor 阻塞的线程重新尝试获取 Monitor 的所有权。

总结

通过注释 1 和注释 2 可知,synchronized 的实现原理,底层是通过 Monitor 的对象来完成,其实 wait 和 notify 等方法也依赖 Monitor,这就是为什么 wait 和 notify 方法必须要在同步方法内调用,否则会抛出 java.lang.IllegalMonitorStateException 的原因。

如果程序正常执行则按上述描述即可完成,如果程序在同步方法内发生异常,代码则会走注释 3,在注释 3 可以看到 MONITOREXIT 指令,也就是 synchronized 已经处理异常情况下的退出。

注:方法级别的 synchronized 不会在字节码指令中有所体现,而是在常量池中增加了ACC_SYNCHRONIZED标识符,JVM 就是通过该标识符来实现同步的,方法调用时,JVM 会判断方法的ACC_SYNCHRONIZED是否被设置,如果被设置,线程执行方法前会先获取 Monitor 所有权,执行完方法后再释放 Monitor 所有权,本质是一样的。

synchronized 原理进阶

轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。

轻量级锁对使用者是透明的,即语法仍然是 synchronized

假设有两个方法同步块,利用同一个对象加锁

static final Object obj = new Object();

public static void method1() {
    synchronized (obj) { // 同步块 A
        method2();
    }
}

public static void method2() {
    synchronized (obj) { // 同步块 B
    }
}

创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word





图 3

让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录





图 4

如果 cas 替换成功,对象头中存储了锁记录地址和状态 00,表示由该线程给对象加锁,这是图示如下





图 5

如果 cas 失败,有两种情况

•如果是其它线程已经持有了该 Object 的轻量级锁,这是表明有竞争,进入锁膨胀过程

•如果是自己线程执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的技术





图 6

当退出 synchronized 代码块(解锁时),如果有取值为 null 的锁记录,表示由重入,这是重置锁记录,表示重入技术减一





图 7

当退出 synchronized 代码块(解锁时),锁记录的值不为 null,这时使用 cas 将 Mark Word 的值回复给对象头

•成功,则解锁成功

•失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

锁膨胀

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

当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁





图 8

这是 Thread-1 加轻量级锁失败,进入锁膨胀流程

•即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址

•然后自己进入 Monitor 的 EntryList BLOCKED





图 9

当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败,这是会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步,释放了锁),这是当前线程就可以避免阻塞。

自旋重试成功的情况

线程 1(core 1 上) 对象 Mark 线程 2(core 2 上)
- 10(重量锁) -
访问同步块,获取 Monitor 10(重量锁)重量锁指针 -
成功(加锁) 10(重量锁)重量锁指针 -
执行同步块 10(重量锁)重量锁指针 -
执行同步块 10(重量锁)重量锁指针 访问同步块,获取 Monitor
执行同步块 10(重量锁)重量锁指针 自旋重试
执行完毕 10(重量锁)重量锁指针 自旋重试
成功(解锁) 01(无锁) 自旋重试
- 10(重量锁)重量锁指针 成功(加锁)
- 10(重量锁)重量锁指针 执行同步块
-

自旋重试失败的情况

线程 1(core 1 上) 对象 Mark 线程 2(core 2 上)
- 10(重量锁) -
访问同步块,获取 Monitor 10(重量锁)重量锁指针 -
成功(加锁) 10(重量锁)重量锁指针 -
执行同步块 10(重量锁)重量锁指针 -
执行同步块 10(重量锁)重量锁指针 访问同步块,获取 Monitor
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 阻塞
-

•自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。

•在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。

•Java 7 之后不能控制是否开启自旋功能。

偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。

Java 6 中引入了偏向锁做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS,以后只要不发生竞争,这个对象就归该线程所有。

注:

Java 15 之后废弃偏向锁,默认是关闭,如果想使用偏向锁,配置-XX:+UseBiasedLocking 启动参数。

启动偏向锁之后,偏向锁有一个延迟生效的机制,这是因为 JVM 启动时会进行一系列的复杂活动,比如装载配置,系统类初始化等等。在这个过程中会使用大量 synchronized 关键字对对象加锁,且这些锁大多数都不是偏向锁。为了减少初始化时间,JVM 默认延时加载偏向锁。这个延时的时间大概为 4s 左右,具体时间因机器而异。当然我们也可以设置 JVM 参数 -XX:BiasedLockingStartupDelay=0 来取消延时加载偏向锁。

例如:

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) {
    }
}

如果关闭偏向锁,使用轻量锁情况:





图 10

开启偏向锁,使用偏向锁情况:





图 11

偏向状态

回忆一下对象头格式

Mark Word (32 bits) State
identity_hashcode:25 age:4 biased_lock:1 lock:2 Normal
thread:23 epoch:2 age:4 biased_lock:1 lock:2 Biased
ptr_to_lock_record:30 lock:2 LightweightLocked
ptr_to_heavyweight_monitor:30 lock:2 HeavyweightLocked
 lock:2 Marked for GC

一个对象创建时:

•如果开启了偏向锁(默认开启),那么对象创建后,Mark Word 值为 0x05,也就是最后是 3 位为 101,这是它的 thread、epoch、age 都为 0

•如果没有开启偏向锁,那么对象创建后,Mark Word 值为 0x01,也就是最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值

我们来验证下,使用 jol 第三方工具,以及对工具打印对象头做了一个处理,让对象头开起来更简便:

测试代码

public synchronized static void main(String[] args){
    log.info("{}", toSimplePrintable(object));
}

开启偏向锁的情况下

打印的数据如下(由于 Java15 之后偏向锁废弃,因此打开偏向锁打印会警告)

17:15:17 [main] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101

最后为 101,其他都为 0,验证了上述第一条。

可能你又要问了,我这也没使用 synchronized 关键字呀,那不也应该是无锁么?怎么会是偏向锁呢?

仔细看一下偏向锁的组成,对照输出结果红色划线位置,你会发现占用 thread 和 epoch 的 位置的均为 0,说明当前偏向锁并没有偏向任何线程。此时这个偏向锁正处于可偏向状态,准备好进行偏向了!你也可以理解为此时的偏向锁是一个特殊状态的无锁

关闭偏向锁的情况下

打印的数据如下

17:18:32 [main] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

最后为 001,其它都是 0,验证了上述第二条。

接下来验证加锁的情况,代码如下:

private static Object object = new Object();
public synchronized static void main(String[] args){
    new Thread(()->{
        log.info("{}", "synchronized前");
        log.info("{}", toSimplePrintable(object));
        synchronized (object){
            log.info("{}", "synchronized中");
            log.info("{}", toSimplePrintable(object));
        }
        log.info("{}", "synchronized后");
        log.info("{}", toSimplePrintable(object));
    },"t1").start();
}

开启偏向锁的情况,打印数据如下

17:24:05 [t1] c.MyClass03 - synchronized 前

17:24:05 [t1] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101

17:24:05 [t1] c.MyClass03 - synchronized 中

17:24:05 [t1] c.MyClass03 - 00000000 00000000 00000000 00000001 00001110 00000111 01001000 00000101

17:24:05 [t1] c.MyClass03 - synchronized 后

17:24:05 [t1] c.MyClass03 - 00000000 00000000 00000000 00000001 00001110 00000111 01001000 00000101

使用了偏向锁,并记录了线程的值(101 前面的一串数字),但是处于偏向锁的对象解锁后,线程 id 仍存储于对象头中。

关闭偏向锁的情况,打印数据如下

17:28:24 [t1] c.MyClass03 - synchronized 前

17:28:24 [t1] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

17:28:24 [t1] c.MyClass03 - synchronized 中

17:28:24 [t1] c.MyClass03 - 00000000 00000000 00000000 00000001 01110000 00100100 10101001 01100000

17:28:24 [t1] c.MyClass03 - synchronized 后

17:28:24 [t1] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

使用轻量锁(最后为 000),并且记录了占中存储的锁信息地址(000 前面一串数字),同步块结束后恢复到原先状态(因为没有使用 hashcode,所以 hashcode 值为 0)。

偏向锁撤销

在真正讲解偏向撤销之前,需要和大家明确一个概念——偏向锁撤销和偏向锁释放是两码事。

•撤销:笼统的说就是多个线程竞争导致不能再使用偏向模式的时候,主要是告知这个锁对象不能再用偏向模式

•释放:和你的常规理解一样,对应的就是 synchronized 方法的退出或 synchronized 块的结束

何为偏向撤销?

从偏向状态撤回原有的状态,也就是将 MarkWord 的第 3 位(是否偏向撤销)的值,从 1 变回 0

如果只是一个线程获取锁,再加上「偏心」的机制,是没有理由撤销偏向的,所以偏向的撤销只能发生在有竞争的情况下

撤销-hashcode 调用

调用了对象的 hashcode 会导致偏向锁被撤销:

•轻量级锁会在锁记录中记录 hashcode

•重量级锁会在 Monitor 中记录 hashcode

测试代码如下

private static Object object = new Object();
public synchronized static void main(String[] args){
    object.hashCode();//调用hashcode
    new Thread(()->{
        log.info("{}", "synchronized前");
        log.info("{}", toSimplePrintable(object));
        synchronized (object){
            log.info("{}", "synchronized中");
            log.info("{}", toSimplePrintable(object));
        }
        log.info("{}", "synchronized后");
        log.info("{}", toSimplePrintable(object));
    },"t1").start();
}

打印如下:

17:36:05 [t1] c.MyClass03 - synchronized 前

17:36:06 [t1] c.MyClass03 - 00000000 00000000 00000000 01011111 00100001 00001000 10110101 00000001

17:36:06 [t1] c.MyClass03 - synchronized 中

17:36:06 [t1] c.MyClass03 - 00000000 00000000 00000000 00000001 01101110 00010011 11101001 01100000

17:36:06 [t1] c.MyClass03 - synchronized 后

17:36:06 [t1] c.MyClass03 - 00000000 00000000 00000000 01011111 00100001 00001000 10110101 00000001

撤销 - 其它线程使用对象

当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁。

测试代码如下

private static void test2() {
    Thread t1 = new Thread(() -> {
        synchronized (object) {
            log.info("{}", toSimplePrintable(object));
        }
        synchronized (MyClass03.class) {
            MyClass03.class.notify();//t1执行完之后才通知t2执行
        }
    }, "t1");
    t1.start();
    Thread t2 = new Thread(() -> {
        synchronized (MyClass03.class) {
            try {
                MyClass03.class.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.info("{}", toSimplePrintable(object));
        synchronized (object) {
            log.info("{}", toSimplePrintable(object));
        }
        log.info("{}", toSimplePrintable(object));
    }, "t2");
    t2.start();
}

打印数据如下

17:51:38 [t1] c.MyClass03 - 00000000 00000000 00000000 00000001 01000111 00000000 11101000 00000101

17:51:38 [t2] c.MyClass03 - 00000000 00000000 00000000 00000001 01000111 00000000 11101000 00000101

17:51:38 [t2] c.MyClass03 - 00000000 00000000 00000000 00000001 01111000 00100000 01101001 01010000

17:51:38 [t2] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

可以看到线程 t1 是使用偏向锁,线程 t2 使用锁之前是一样的,但是一旦使用了锁,便升级为轻量级锁,执行完同步代码之后,恢复成撤销偏向锁的状态。

撤销 - 调用 wait/notify

代码如下

private static void test3(){
    Thread t1 = new Thread(() -> {
        log.info("{}", toSimplePrintable(object));
        synchronized (object) {
            log.info("{}", toSimplePrintable(object));
            try {
                object.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("{}", toSimplePrintable(object));
        }
    }, "t1");
    t1.start();
    new Thread(() -> {
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (object) {
            log.debug("notify");
            object.notify();
        }
    }, "t2").start();
}

打印数据如下

17:57:57 [t1] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101

17:57:57 [t1] c.MyClass03 - 00000000 00000000 00000000 00000001 00001111 00001100 11010000 00000101

17:58:02 [t2] c.MyClass03 - notify

17:58:02 [t1] c.MyClass03 - 00000000 00000000 01100000 00000000 00000011 11000001 10000010 01110010

调用 wait 和 notify 得是用 Monitor,所以会从偏向锁升级为重量级锁。

批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这是偏向了线程 t1 的对象仍然有机会重新偏向 t2,重偏向会重置对象的 Thread ID。

当撤销偏向锁阈值超过 20 次后,JVM 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程。

代码如下

public static class Dog{}

private static void test4() {
    Vector<Dog> list = new Vector<>();
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 30; i++) {
            Dog d = new Dog();
            list.add(d);
            synchronized (d) {
                log.info("{}", i+"\t"+toSimplePrintable(d));
            }
        }
        synchronized (list) {
            list.notify();
        }
    }, "t1");
    t1.start();
    Thread t2 = new Thread(() -> {
        synchronized (list) {
            try {
                list.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.debug("===============> ");
        for (int i = 0; i < 30; i++) {
            Dog d = list.get(i);
            log.info("{}", i+"\t"+toSimplePrintable(d));
            synchronized (d) {
                log.info("{}", i+"\t"+toSimplePrintable(d));
            }
            log.info("{}", i+"\t"+toSimplePrintable(d));
        }
    }, "t2");
    t2.start();
}

打印如下





图 12

另外我在测试的是否发现一个线程,当对象是普通类(如 Dog)时,重偏向的阈值就是 20,也就是第 21 次开启了偏向锁,但是如果把普通类替换成 Object 时,重偏向的阈值就是 9,也就是第 10 次开启了偏向锁并重偏向(如图 13),这是怎么回事儿,有了解的同学可以评论交流下。





图 13

批量撤销

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

代码如下

static Thread t1, t2, t3;

private static void test6() throws InterruptedException {
    Vector<Dog> list = new Vector<>();
    int loopNumber = 40;
    t1 = new Thread(() -> {
        for (int i = 0; i < loopNumber; i++) {
            Dog d = new Dog();
            list.add(d);
            synchronized (d) {
                log.info("{}", i + "\t" + toSimplePrintable(d));
            }
        }
        LockSupport.unpark(t2);
    }, "t1");
    t1.start();
    t2 = new Thread(() -> {
        LockSupport.park();
        log.debug("===============> ");
        for (int i = 0; i < loopNumber; i++) {
            Dog d = list.get(i);
            log.info("{}", i + "\t" + toSimplePrintable(d));
            synchronized (d) {
                log.info("{}", i + "\t" + toSimplePrintable(d));
            }
            log.info("{}", i + "\t" + toSimplePrintable(d));
        }
        LockSupport.unpark(t3);
    }, "t2");
    t2.start();
    t3 = new Thread(() -> {
        LockSupport.park();
        log.debug("===============> ");
        for (int i = 0; i < loopNumber; i++) {
            Dog d = list.get(i);
            log.info("{}", i + "\t" + toSimplePrintable(d));
            synchronized (d) {
                log.info("{}", i + "\t" + toSimplePrintable(d));
            }
            log.info("{}", i + "\t" + toSimplePrintable(d));
        }
    }, "t3");
    t3.start();
    t3.join();
    log.info("{}", toSimplePrintable(new Dog()));
}

打印如下





图 14


↙↙↙阅读原文可查看相关链接,并与作者交流