1. 什么是死锁?
-
当两个线程互相持有对方所需要的资源,又不主动释放,导致所有线程都无法继续前进,导致程序陷入无尽的阻塞,这就是死锁,如果多个线程之间的依赖关系是环形,也可以形成死锁;死锁发生在并发中,单线程是不会发生死锁的;
-
死锁的影响:在不同系统中是不一样的,这取决于系统对死锁的处理能力
- 数据库中:检测并放弃事务可以修复死锁
- JVM中:无法自动处理
-
必然发生死锁的例子:
public class DeadLock {
private static Object lockA = new Object();
private static Object lockB = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "获取到lockA");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在尝试获取lockB...");
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "获取到lockB");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "获取到lockB");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在尝试获取lockA...");
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "获取到lockA");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
t1.start();
t2.start();
}
}
Thread-1获取到lockB
Thread-0获取到lockA
Thread-0正在尝试获取lockB...
Thread-1正在尝试获取lockA...
- 死锁发生的四个必要条件:
- 互斥条件:一把锁在同一时刻只能被同一个线程占用;
- 请求与保持条件:持有一把锁同时请求另外一把锁;
- 不剥夺条件:自己持有的一把锁不能被抢走;
- 循环等待条件:从头开始,一个等待另外一个释放锁,构成环路;
2. 如何定位死锁?
- 用jps查到发生死锁线程的pid,然后用jstack pid的方法打印出发生死锁的的栈信息;
- 用ThreadMXBean对象检测死锁,可以根据不同的情况,一旦发生死锁,可以在代码中采取应对措施;
public class DeadLock {
private static Object lockA = new Object();
private static Object lockB = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "获取到lockA");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在尝试获取lockB...");
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "获取到lockB");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "获取到lockB");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在尝试获取lockA...");
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "获取到lockA");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
t1.start();
t2.start();
Thread.sleep(2000);
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads != null && deadlockedThreads.length > 0) {
for (int i = 0; i < deadlockedThreads.length; i++) {
ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);
System.out.println("发现死锁:" + threadInfo.getThreadName());
}
}
}
}
Thread-1获取到lockB
Thread-0获取到lockA
Thread-1正在尝试获取lockA...
Thread-0正在尝试获取lockB...
发现死锁:Thread-1
发现死锁:Thread-0
2. 如何修复死锁?
2.1 避免策略:避免获取锁的顺序相反
- 转账换序方案
public class TransferAccount implements Runnable {
private User from;
private User to;
public TransferAccount(User from, User to) {
this.from = from;
this.to = to;
}
@Override
public void run() {
try {
transfer(from, to, 100.0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void transfer(User from, User to, double money) throws InterruptedException {
synchronized (from) {
Thread.sleep(500);
synchronized (to) {
double fromBalance = from.getBalance() - money;
if (fromBalance < 0) {
throw new RuntimeException("余额不足,转账失败");
}
from.setBalance(fromBalance);
to.setBalance(to.getBalance() + money);
System.out.println(from.getName() + "成功转账给" + to.getName() + money + "元," + from.getName() + "剩余" + fromBalance + "元");
}
}
}
public static void main(String[] args) throws InterruptedException {
User userA = new User(1, "用户A", 300.0);
User userB = new User(2, "用户B", 300.0);
Runnable r1 = new TransferAccount(userA, userB);
Runnable r2 = new TransferAccount(userB, userA);
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
Thread.sleep(1000);
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads != null && deadlockedThreads.length > 0) {
for (int i = 0; i < deadlockedThreads.length; i++) {
ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);
System.out.println("发现死锁:" + threadInfo.getThreadName());
}
}
}
}
发现死锁:Thread-1
发现死锁:Thread-0
以上代码展示了转账过程中发生死锁的情况,由于用户A和用户B相互转账的过程中,线程t1持有用户A的锁,线程t2持有用户B的锁,同时两个线程也互相请求对方的锁,满足了死锁的四个必要条件:互斥、请求与等待、不可剥夺以及循环等待,造成成了死锁。
避免策略是指避免t1线程和t2线程获取锁的顺序相反,死锁的情况是:t1先获取A锁,再请求B锁,t2先获取B锁,在请求A锁,顺序反了;解决死锁的策略是:t1先获取A锁,再请求B锁,t2也先获取A锁,再请求B锁,这样t1已经获取A锁时,t2发生阻塞,等待t1释放A锁,就不会发生死锁。
public class TransferAccount implements Runnable {
private User from;
private User to;
public TransferAccount(User from, User to) {
this.from = from;
this.to = to;
}
@Override
public void run() {
try {
transfer(from, to, 100.0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void transfer(User from, User to, double money) throws InterruptedException {
if (from.getId() < to.getId()) {
synchronized (from) {
Thread.sleep(500);
synchronized (to) {
fromTransferTo(from, to, money);
}
}
} else {
synchronized (to) {
Thread.sleep(500);
synchronized (from) {
fromTransferTo(from, to, money);
}
}
}
}
public void fromTransferTo(User from ,User to, double money) {
double fromBalance = from.getBalance() - money;
if (fromBalance < 0) {
throw new RuntimeException("余额不足,转账失败");
}
from.setBalance(fromBalance);
to.setBalance(to.getBalance() + money);
System.out.println(from.getName() + "成功转账给" + to.getName() + money + "元," + from.getName() + "剩余" + fromBalance + "元");
}
public static void main(String[] args) throws InterruptedException {
User userA = new User(1, "用户A", 300.0);
User userB = new User(2, "用户B", 300.0);
Runnable r1 = new TransferAccount(userA, userB);
Runnable r2 = new TransferAccount(userB, userA);
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
Thread.sleep(2000);
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads != null && deadlockedThreads.length > 0) {
for (int i = 0; i < deadlockedThreads.length; i++) {
ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);
System.out.println("发现死锁:" + threadInfo.getThreadName());
}
}
}
}
class User {
private Integer id;
private String name;
private double balance;
public User(Integer id, String name, double balance) {
this.id = id;
this.name = name;
this.balance = balance;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
}
上述代码通过比较两个用户的id大小来决定获取锁的顺序,用户A的ID为1,用户B的ID为2,在A给B转账过程中,让t1线程先获取A的锁再获取B的锁;在B给A转账的过程中,也是让t2线程先获取A的锁,再获取B的锁,这样就解决了死锁的问题。
2. 哲学家换手方案
public class Philosopher implements Runnable {
private Object left;
private Object right;
public Philosopher(Object left, Object right) {
this.left = left;
this.right = right;
}
@Override
public void run() {
while (true) {
doAction("思考中...");
synchronized (left) {
doAction("拿起左筷子");
synchronized (right) {
doAction("拿起右筷子,开吃...");
doAction("放下右筷子");
}
doAction("放下左筷子");
}
}
}
private void doAction(String s) {
System.out.println(s);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Philosopher[] philosophers = new Philosopher[5];
Object[] chopsticks = new Object[philosophers.length];
for (int i = 0; i < philosophers.length; i++) {
chopsticks[i] = new Object();
}
for (int i = 0; i < philosophers.length; i++) {
Object left = chopsticks[i];
Object right = chopsticks[(i+1) % chopsticks.length];
philosophers[i] = new Philosopher(left, right);
// if ((i+1) == philosophers.length) {
// philosophers[i] = new Philosopher(right, left);
// } else {
// philosophers[i] = new Philosopher(left, right);
// }
new Thread(philosophers[i], "哲学家" + (i+1)+ "号").start();
}
}
}
思考中...
思考中...
思考中...
思考中...
思考中...
拿起左筷子
拿起左筷子
拿起左筷子
拿起左筷子
拿起左筷子
如上所示,每个哲学家在思考后都拿起了左筷子,此时都在请求右筷子,构成了环路等待的情况,发生了死锁,我们只需要按照注释代码中写的,改变任意一个哲学家拿筷子的顺序,打破环路等待,就可以避免死锁。
2.2 剥夺策略:定期检测死锁,如果有就剥夺某一个资源
- 每次调用锁都记录,定期检查锁的调用链路图中是否存在环路,一旦发生死锁,用死锁恢复机制进行恢复
- 进程终止
- 逐个终止线程,直到死锁消除
- 终止顺序:
- 优先级,是前台交互还是后台处理
- 根据已占用资源、还需要的资源
- 已经运行时间
- 资源抢占
- 把已经发出去的锁给收回来
- 让线程回退几步,这样就不用结束整个线程,成本比较低
- 缺点:可能同一个线程一直被抢占,那就造成饥饿
- 进程终止
3. 实际项目中避免死锁问题
- 设置超时时间
- Lock的tryLock(long timeout, TimeUnit unit)
- synchronized不具备尝试锁的能力
- 造成超时的可能性多:发生了死锁、线程陷入死循环、线程执行很慢
- 获取锁失败:日志、邮件、重启等
- 多使用并发类
- 能使用同步代码块,就不使用同步方法:自己指定锁对象
- 避免锁的嵌套
- 尽量不要几个功能用同一把锁
4. 其他活性故障
- 活锁
- 线程没阻塞,始终在运行,程序却得不到进展,始终在做重复的事
- 死锁:每个哲学家都拿着左手的餐叉,永远都在等右边的餐叉
- 活锁:同时拿起左边的餐叉,同时等待五分钟,同时放下餐叉,又同时等五分钟,又同时拿起这些餐叉
- 在实际的计算机问题中,缺乏餐叉可以类比为缺乏共享资源
- 解决:加入随机因素
- 消息队列
- 策略:消息如果处理失败,就放在队列开头重试
- 由于依赖服务出了问题,处理该消息一直失败
- 没阻塞,但是程序无法继续
- 解决:放到队列尾部、重试限制,重试了几次后还失败,就把失败的消息写入到数据库中,触发报警机制
- 饥饿
- 当线程需要某些资源例如CPU,但是却始终得不到
- 线程的优先级设置的过低,或者其它线程始终持有我需要的锁一直不释放都会导致饥饿
- 饥饿导致响应性变差。比如,浏览器有个线程处理前台响应,而后台线程负责下载图片和文件等等。在这种情况下,如果后台线程把CPU资源都占用了,那么前台线程将无法得到很好的执行,这会导致用户的体验很差
- 线程优先级:10个级别,默认是5,默认子线程会继承父线程,也是5
- 程序设计不应该依赖于优先级
- 不同操作系统不一样
- 优先级会被操作系统改变