线程活跃性问题

概述

理想情况下,我们希望线程能一直处于运行(Runnable)状态,但是会由于一些因素。这些由于资源稀缺或者程序自身问题导致线程无法一直处于 Runnable 状态运行下去,又或者因为线程处于 Runnable 状态但是其要执行的任务一直无法进展的现象就被称为线程活跃性问题活性故障

死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
Object lockA = new Object();
Object lockB = new Object();
new Thread(()->{
synchronized (lockA){
System.out.println("获取了A锁");
synchronized (lockB){
System.out.println("尝试获取B锁...");
}
}
}).start();
new Thread(()->{
synchronized (lockB){
System.out.println("获取了B锁");
synchronized (lockA){
System.out.println("尝试获取A锁...");
}
}
}).start();
}

死锁的四个条件:

  • 互斥:共享资源 X 和 Y 只能被一个线程占用;
  • 占有且等待:线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
  • 不可抢占:其他线程不能强行抢占线程 T1 占有的资源;
  • 循环等待:线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。

破坏死锁:即破坏死锁的条件之一

  • 互斥:没办法去破坏,因为这是程序必须要保证的。

  • 对于“占用且等待”这个条件:我们可以一次性申请所有的资源,这样就不存在等待了。

  • 对于“不可抢占”这个条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。

  • 对于“循环等待”这个条件:可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。

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

  • jps查看进程
  • 利用jstack id定位死锁

活锁

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束。(可以添加随机睡眠时间来解决)

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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(100);
count--;
}
}, "t1").start();
new Thread(() -> {
// 期望超过 20 退出循环
while (count < 20) {
sleep(200);
count++;
}
}, "t2").start();
}
}

饥饿

饥饿指的是一个线程由于某种原因始终得不到CPU调度执行,也不能够结束。

原因:

  • 原因之一是不同线程或线程组之间的线程优先级不正确。
  • 另一个可能的原因可能是使用非终止循环(无限循环)或在特定资源上等待过多时间,同时保留了其他线程所需的关键锁。

解决方案:

  • 通常建议尽量避免修改线程优先级,因为这是导致线程饥饿的主要原因。一旦开始使用线程优先级调整应用程序,它就会与特定平台紧密耦合,并且还会带来线程匮乏的风险。
  • 利用公平锁公平地分配资源。
  • 保证资源充足。
  • 避免持有锁的线程长时间执行。