关于java:开发宝典Java并发系列教程

33次阅读

共计 14216 个字符,预计需要花费 36 分钟才能阅读完成。

作者:京东批发 刘跃明

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]

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

正文完
 0