死锁
- 死锁的三种场景:线程死锁、线程池死锁、资源连接池死锁
- 线程死锁:多个线程分别等待对方持有的锁,比如下面代码,线程Thread-1持有锁a并尝试获取锁b,Thread-2持有锁b并尝试获取锁a,导致两个线程都不能获取需要的锁
@Test
public void testLock1() throws InterruptedException {
Object a = new Object();
Object b = new Object();
new Thread(() -> {
try {
synchronized (a) {
Thread.sleep(1000l);
System.out.println("now i in thread-1 locka");
synchronized (b) {
System.out.println("now i in thread-1 lockb");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Thread-1").start();
Thread thread2 = new Thread(() -> {
try {
synchronized (b) {
Thread.sleep(1000l);
System.out.println("now i in thread-2 lockb");
synchronized (a) {
System.out.println("now i in thread-2 locka");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Thread-2");
thread2.start();
// 让thread2先执行完成,否则主线程结束看不到效果
thread2.join();
}
// --------------控制台打印结果 程序堵塞--------------
now i in thread-1 locka
now i in thread-2 lockb
// ---------------执行以下命令检查------------------
//检查java进程
➜ demo git:(seata) ✗ jps
1600 Launcher
4880 JUnitStarter
//打印进程线程,结果如下图
➜ demo git:(seata) ✗ jstack 4880
上图可以发现Thread-1和Thread-2互相持有对方期望得到的锁
解决办法:
1.超时放弃
2.多线程以相同的顺序尝试获取锁
- 线程池死锁:同一个线程池中的任务,在执行中任务中使用该线程池做其他任务,使得线程池中任务形成一种嵌套依赖,并且需要线程池返回结果,如下代码
@Test
public void testPool() throws Exception {
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<Long> r1 = executorService.submit(new Callable<Long>() {
@Override
public Long call() throws Exception {
System.out.println("start T1");
Thread.sleep(1000l);
Future<Long> r2 = executorService.submit(new Callable<Long>() {
@Override
public Long call() throws Exception {
System.out.println("start T2");
return -2L;
}
});
System.out.println("r2 -> " + r2.get());
System.out.println("end f1");
return -1L;
}
});
System.out.println("r1 -> " + r1.get());
Thread.sleep(2000l);
System.out.println("end-");
}
因为结果r1和r2都被后续代码使用,所以两个任务都会等待线程池中所有任务都完成并成功返回才能继续,导致了死锁,程序只打印出start T1
// 注释该行,因为内部的任务一直无法完成 // 导致System.out.println("r2 -> " + r2.get()); // System.out.println("end f1");逻辑无法执行 输出start T1;end- System.out.println("r1 -> " + r1.get()); // 注释该行,程序正常完成输出 start T1;end f1;start T2;end- System.out.println("r2 -> " + r2.get());
这也说明了一点——线程池返回的Future在没有被使用的情况下,线程池并不会等待所有线程任务完成
使用jstack查看线程转储信息如下(省略部分)虽然不知道红框中的具体指的是什么但是箭头所指的代码位置是对线程池返回结果的Future的使用,分别对应r2.get()和r1.get()
解决办法:
1.像上面一样如果不需要使用返回结果尽量不用
2.万一躲不过第一点并且线程池内部仍然要使用线程池任务建议重新不要使用同一个线程池
3.增加线程池线程大小,Executors.newSingleThreadExecutor创建单线程的线程池,外部任务因为使用内部任务的Future结果所以必须等内部任务完成才释放线程,而内部任务在等待队列中等待线程池唯一的线程被释放才能开始执行。如果增加线程数量,内部任务不会和外部任务争夺线程资源,各自完成后返回从而解决了死锁问题
本来想写出一个多线程导致活锁的例子,写完感觉应该是死锁的一种形式,代码如下
@Test
public void testLiveLock() {
Thread t1 = new MyThread(Thread.currentThread());
t1.start();
try {
t1.join();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("end...");
}
class MyThread extends Thread {
private Thread specifiedThread;
public MyThread(Thread specifiedThread) {
this.specifiedThread = specifiedThread;
}
@Override
public void run() {
try {
System.out.println("son start....");
specifiedThread.join();
System.out.println("son end....");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 资源连接池死锁:和线程死锁类似,不过锁变成了资源池,直接引用参考文章的内容如下
在网络连接池也会发生死锁,假设此时有两个线程A和B,两个数据库连接池N1和N2,连接池大小都只有1,如果线程A按照先N1后N2的顺序获得网络连接,而线程B按照先N2后N1的顺序获得网络连接,并且两个线程在完成执行之前都不释放自己已经持有的链接,因此也造成了死锁
活锁
- 概念:没有产生线程堵塞,但是程序无法进行,
- 比如消息失败重试,如果这个消息格式错误无法解析导致这个消息一直被重试
- 再如两个线程互相谦让导致程序无法进行,比如夫妻谦让吃饭的例子
public class LiveLockTest {
static class Spoon {
private Diner owner;
public Spoon(Diner owner) {
this.owner = owner;
}
public Diner getOwner() {
return this.owner;
}
public synchronized void setOwner(Diner owner) {
this.owner = owner;
}
public synchronized void use() {
System.out.printf("%s has eaten!", owner.getName());
}
}
@Data
static class Diner {
private String name;
private boolean isHungry;
public Diner(String name) {
this.name = name;
isHungry = true;
}
public void eatWith(Spoon spoon, Diner spouse) {
while (isHungry) {
//如果没有勺子 则等待
if (spoon.owner != this) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
continue;
}
//如果配偶饿了,将勺子使用权给配偶
if (spouse.isHungry) {
System.out.printf(
"%s: You eat first my darling %s!%n",
name, spouse.getName());
spoon.setOwner(spouse);
continue;
}
//后面代码永远无法执行,并且因为use一直没有执行 自己饿的状态也无法改变
spoon.use();
isHungry = false;
System.out.printf(
"%s: I am stuffed, my darling %s!%n",
name, spouse.getName());
spoon.setOwner(spouse);
}
}
}
public static void main(String[] args) {
final Diner husband = new Diner("Bob");
final Diner wife = new Diner("Alice");
final Spoon s = new Spoon(husband);
new Thread(new Runnable() {
public void run() {
husband.eatWith(s, wife);
}
}).start();
new Thread(new Runnable() {
public void run() {
wife.eatWith(s, husband);
}
}).start();
}
}
饥饿
- 概念:线程优先级不一致,导致优先级高的线程能够插队并优先执行,如果优先级高的线程一直强占优先级低的线程资源,导致低优先级的线程一直无法执行,造成低优先级线程饥饿,饥饿和死锁不一样,死锁是线程阻塞饥饿低优先级线程获得cpu资源时仍然可以执行,如下代码执行结果,低优先级counter2也被执行,只是counter1结果往往会比counter2大
@Test
public void testHunger() throws Exception {
int CPUN = Runtime.getRuntime().availableProcessors() << 1;
Counter counter1 = new Counter();
for (int i = 0; i < CPUN; i++) {
Thread preferentialThread = new Thread(counter1, "R-Thread");
preferentialThread.setPriority(10);
preferentialThread.start();
}
Counter counter2 = new Counter();
for (int i = 0; i < CPUN; i++) {
Thread patientThread = new Thread(counter2, "A-Thread");
patientThread.setPriority(1);
patientThread.start();
}
Thread.sleep(10000l);
System.out.println("counter1-->" + counter1.getCounter());
System.out.println("counter2-->" + counter2.getCounter());
}
class Counter implements Runnable {
private long counter = 0;
@Override
public void run() {
while (true) {counter++;}
}
public long getCounter() {
return this.counter;
}
}
//-------结果----
counter1-->1129061922
counter2-->797618732
无锁
- 概念:对资源没有锁定,所有线程都能够访问并修改同一个资源,但是同时只能有一个线程修改成功,典型的案例就是CAS
参考:
https://juejin.im/post/5aaf6ee76fb9a028d3753534#heading-4
https://codeday.me/bug/20170609/23317.html