Java并发编程之死锁

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...
  • 死锁发生的四个必要条件:
    1. 互斥条件:一把锁在同一时刻只能被同一个线程占用;
    2. 请求与保持条件:持有一把锁同时请求另外一把锁;
    3. 不剥夺条件:自己持有的一把锁不能被抢走;
    4. 循环等待条件:从头开始,一个等待另外一个释放锁,构成环路;

2. 如何定位死锁?

  1. 用jps查到发生死锁线程的pid,然后用jstack pid的方法打印出发生死锁的的栈信息;
  2. 用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 避免策略:避免获取锁的顺序相反

  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 剥夺策略:定期检测死锁,如果有就剥夺某一个资源

  • 每次调用锁都记录,定期检查锁的调用链路图中是否存在环路,一旦发生死锁,用死锁恢复机制进行恢复
    1. 进程终止
      • 逐个终止线程,直到死锁消除
      • 终止顺序:
        1. 优先级,是前台交互还是后台处理
        2. 根据已占用资源、还需要的资源
        3. 已经运行时间
    2. 资源抢占
      • 把已经发出去的锁给收回来
      • 让线程回退几步,这样就不用结束整个线程,成本比较低
      • 缺点:可能同一个线程一直被抢占,那就造成饥饿

3. 实际项目中避免死锁问题

  1. 设置超时时间
    • Lock的tryLock(long timeout, TimeUnit unit)
    • synchronized不具备尝试锁的能力
    • 造成超时的可能性多:发生了死锁、线程陷入死循环、线程执行很慢
    • 获取锁失败:日志、邮件、重启等
  2. 多使用并发类
  3. 能使用同步代码块,就不使用同步方法:自己指定锁对象
  4. 避免锁的嵌套
  5. 尽量不要几个功能用同一把锁

4. 其他活性故障

  • 活锁
    • 线程没阻塞,始终在运行,程序却得不到进展,始终在做重复的事
    • 死锁:每个哲学家都拿着左手的餐叉,永远都在等右边的餐叉
    • 活锁:同时拿起左边的餐叉,同时等待五分钟,同时放下餐叉,又同时等五分钟,又同时拿起这些餐叉
    • 在实际的计算机问题中,缺乏餐叉可以类比为缺乏共享资源
    • 解决:加入随机因素
  • 消息队列
    • 策略:消息如果处理失败,就放在队列开头重试
    • 由于依赖服务出了问题,处理该消息一直失败
    • 没阻塞,但是程序无法继续
    • 解决:放到队列尾部、重试限制,重试了几次后还失败,就把失败的消息写入到数据库中,触发报警机制
  • 饥饿
    • 当线程需要某些资源例如CPU,但是却始终得不到
    • 线程的优先级设置的过低,或者其它线程始终持有我需要的锁一直不释放都会导致饥饿
    • 饥饿导致响应性变差。比如,浏览器有个线程处理前台响应,而后台线程负责下载图片和文件等等。在这种情况下,如果后台线程把CPU资源都占用了,那么前台线程将无法得到很好的执行,这会导致用户的体验很差
    • 线程优先级:10个级别,默认是5,默认子线程会继承父线程,也是5
    • 程序设计不应该依赖于优先级
      • 不同操作系统不一样
      • 优先级会被操作系统改变
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值