作者:京东批发 刘跃明
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