线程对象锁被暂用,导致线程无法正常结束问题

案例出自 马士兵MAC课程

案例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Test01 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("t1 start");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 end");
}, "thread-0");
t1.start();

Thread t2 = new Thread(() -> {
synchronized (t1) {
System.out.println("t2 start");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 isAlive: " + t1.isAlive());
}
}, "thread-1");
t2.start();
}
}

案例现象

  1. 在主线程中创建并启动两个线程,线程 t1 睡眠两秒后打印 “t1 end” 后结束,线程 t2 睡眠五秒后打印了 t1 线程的状态

  2. 正常来说,t1 线程睡眠两秒并打印 “t1 end” 后就结束了,因此线程 t2 在打印线程 t1 的存活状态应该为 false

  3. 然而实际结果为 true

    image-20210921222454716

分析原因

​ 1. 线程 t1 在结束的时候需要获取自身对象这把锁,而这把锁被线程 t2 通过 synchronized 持有,因此没有结束

验证

方式一

在 synchronized 之前加上 *t1.join()*。

image-20210921223726569

此时 t1 结束时,t2 并为持有 t1 对象作为锁,可以正常结束,因此 t2 打印 t1 线程状态是否存活为 false。

image-20210921223906639

方式二

在 synchronized 之后加上 *t1.join()*。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Test01 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("t1 start");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 end");
}, "thread-0");
t1.start();

Thread t2 = new Thread(() -> {
synchronized (t1) {
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2 start");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 isAlive: " + t1.isAlive());
}
}, "thread-1");
t2.start();
}
}

image-20210921224107485

分析

  1. 当代码运行到 t1.join() 时,t2 持有的 t1 线程的对象锁,并且等待 t1 线程执行完成才能继续往下执行
  2. 然而 t1 线程需要获取自身对象锁才能结束,只有当 t1 线程结束了 t2 线程才能继续往下执行释放 t1 线程的对象锁
  3. 这段代码为什么可以正常结束而不发生死锁问题呢?

原因

这时我们去关注 join() 方法的源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public final void join() throws InterruptedException {
join(0);
}

public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;

if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}

if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}

我们发现,join() 方法的实现最终是通过循环判断当前线程的活跃状态 *isAlive()*,并调用 wait(0) 方法。而 wait(0) 方法是 Object 的方法,他会让当前线程释放它所持有的锁。因此 t1 线程就可以获取其自身的对象锁成功结束。