活跃性分析

一、死锁

死锁产生的四个必要条件

  • 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
  • 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
  • 环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。

死锁示例

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁

  • t1 线程获得A对象锁,接下来想获取 B对象的锁
  • t2 线程获得B对象锁,接下来想获取 A对象的锁
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
33
34
35
36
37
38
39
40
41
42
43
import lombok.extern.slf4j.Slf4j;

import static site.weiyikai.thread.utils.Sleeper.sleep;

/**
* @author xiaowei
* @date 2022-10-24
* @description 死锁
**/
@Slf4j(topic = "c.TestDeadLock")
public class TestDeadLock {
public static void main(String[] args) {
test1();
}

private static void test1() {
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A) {
log.debug("lock A");
sleep(1);
synchronized (B) {
log.debug("lock B");
log.debug("操作...");
}
}
},"t1");

Thread t2 = new Thread(() -> {
synchronized (B) {
log.debug("lock B");
sleep(0.5);
synchronized (A) {
log.debug("lock A");
log.debug("操作...");
}
}
},"t2");
t1.start();
t2.start();
}
}

运行结果

1
2
3
21:51:08.966 c.TestDeadLock [t2] - lock B
21:51:08.966 c.TestDeadLock [t1] - lock A
// 无限等待

二、定位死锁

检测死锁可以使用jconsole工具,或者使用jps定位进程id,再用jstack 定位死锁。

2.1 jconsole工具检测死锁

(1)选择要监测死锁的进程

(2)选择线程->点击下方检测死锁按钮

(3)产生死锁的线程和信息

2.2 通过jstack 命令定位死锁

(1)通过jps命令定位进程id

1
2
3
4
5
D:\Codes\idea\thread_demo>jps
7456 JConsole
9156 TestDeadLock
1128 Launcher
12264 Jps

(2)通过 jstack 命令定位死锁

1
D:\Codes\idea\thread_demo>jstack 9156

运行部分结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...............
Java stack information for the threads listed above:
===================================================
"t2":
at com.lilinchao.concurrent.demo_03.TestDeadLock.lambda$test1$1(TestDeadLock.java:37)
- waiting to lock <0x00000000d7f90678> (a java.lang.Object)
- locked <0x00000000d7f90688> (a java.lang.Object)
at com.lilinchao.concurrent.demo_03.TestDeadLock$$Lambda$2/1416233903.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"t1":
at com.lilinchao.concurrent.demo_03.TestDeadLock.lambda$test1$0(TestDeadLock.java:26)
- waiting to lock <0x00000000d7f90688> (a java.lang.Object)
- locked <0x00000000d7f90678> (a java.lang.Object)
at com.lilinchao.concurrent.demo_03.TestDeadLock$$Lambda$1/787387795.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.
  • 避免死锁要注意加锁顺序

  • 另外如果由于某个线程进入了死循环,导致其它线程一直等待

    对于这种情况 linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查

三、哲学家就餐问题

有五位哲学家,围坐在圆桌旁。

  • 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
  • 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
  • 如果筷子被身边的人拿着,自己就得等待
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import site.weiyikai.concurrent.utils.Sleeper;
import lombok.extern.slf4j.Slf4j;

/**
* Created by xiaowei
* Date 2022/10/24
* Description 哲学家就餐问题
*/
public class Test05 {


public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("苏格拉底", c1, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c5, c1).start();
}

}

@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
Chopstick left;
Chopstick right;

public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}

@Override
public void run() {
while (true) {
// 尝试获得左手筷子
synchronized (left) {
// 尝试获得右手筷子
synchronized (right) {
eat();
}
}
}
}

private void eat() {
log.debug("eating..."); // 吃饭
Sleeper.sleep(0.5); // 思考
}
}

class Chopstick {
String name;

public Chopstick(String name) {
this.name = name;
}

@Override
public String toString() {
return "筷子{" + name + '}';
}
}

运行结果

1
2
3
4
5
6
7
8
22:54:29.039 c.Philosopher [赫拉克利特] - eating...
22:54:29.039 c.Philosopher [苏格拉底] - eating...
22:54:29.544 c.Philosopher [亚里士多德] - eating...
22:54:29.544 c.Philosopher [阿基米德] - eating...
22:54:30.045 c.Philosopher [阿基米德] - eating...
22:54:30.545 c.Philosopher [赫拉克利特] - eating...
22:54:31.046 c.Philosopher [亚里士多德] - eating...
// 无限等待

使用 jconsole 检测死锁

线程各自持有各自的资源无法释放。

这种线程没有按预期结束,执行不下去的情况,归类为【活跃性】问题,除了死锁以外,还有活锁和饥饿者两种情况。

四、活锁

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束。

活锁是一种情景,当两个或多个线程不停地尝试执行某个操作,但它们的执行被彼此的操作阻塞时,就会出现活锁。例如,在一个多人游戏中,两个角色相遇,他们的行动互相阻止对方移动,最终引发活锁。

在这种情况下,两个线程可以一直卡在互相阻止对方的操作上,导致无法结束程序。解决活锁的方法是引入一些调度机制,在两个线程中有一方先停止操作,让另一方继续执行,然后再轮到其它方停止操作。这样,两个线程可以在不互相阻塞的时候完成任务,从而避免了活锁的发生。

因此,在编程时,需要遵循一些规范,避免出现活锁的情况,以保证线程的正常运行。

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
33
import lombok.extern.slf4j.Slf4j;

import static site.weiyikai.concurrent.utils.Sleeper.sleep;

/**
* @author xiaowei
* @date 2022-10-24
* @description 活锁
**/
@Slf4j(topic = "c.TestLiveLock")
public class TestLiveLock {
static volatile int count = 10;
static final Object lock = new Object();

public static void main(String[] args) {
new Thread(() -> {
// 期望减到 0 退出循环
while (count > 0) {
sleep(0.2);
count--;
log.debug("count: {}", count);
}
}, "t1").start();
new Thread(() -> {
// 期望超过 20 退出循环
while (count < 20) {
sleep(0.2);
count++;
log.debug("count: {}", count);
}
}, "t2").start();
}
}

运行结果

1
2
3
4
5
6
7
8
23:04:21.041 c.TestLiveLock [t1] - count: 9
23:04:21.041 c.TestLiveLock [t2] - count: 9
23:04:21.246 c.TestLiveLock [t2] - count: 10
23:04:21.246 c.TestLiveLock [t1] - count: 9
23:04:21.447 c.TestLiveLock [t2] - count: 10
23:04:21.447 c.TestLiveLock [t1] - count: 9
23:04:21.652 c.TestLiveLock [t2] - count: 10
......................

分析

t1和t2两个线程,一个执行count--,期望减到0的时候退出循环,一个执行count++,期望加到20退出循环,但是永远不能都退出循环。

五、饥饿

很多教程中把饥饿定义为,*一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束*,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题


活跃性分析
http://example.com/2023/03/28/活跃性分析/
作者
程序员小魏
发布于
2023年3月28日
许可协议