【Kill Thread Part.3-1】死锁的概念

【Kill Thread Part.3-1】死锁的概念

一、死锁是什么

1、死锁图解

image-20220128205847942

image-20220128205922186

发生在并发中

互不相让:当两个(或更多)线程(或进程)相互持有对方所需要的资源,又不主动释放,导致所有人都无法继续前进,导致程序陷入无尽的阻塞,这就是死锁。

2、死锁的影响

死锁的影响在不同系统中是不一样的,这取决于系统对死锁的处理能力。

  • 数据库中:检测并放弃事务
  • JVM中:无法自动处理

特点:

  • 几率不高但是危害很大
  • 一旦发生,多是高并发场景,影响用户多
  • 整个系统崩溃、子系统崩溃、性能降低
  • 压力测试无法找出所有潜在的死锁

二、发生死锁的例子

1、最简单的情况

代码

/**
 * 描述:必定发生死锁的情况
 */
public class MustDeadLock implements Runnable{
    int flag = 1;

    //静态对象锁锁
    static Object o1 = new Object();
    static Object o2 = new Object();

    @Override
    public void run() {
        System.out.println("flag = " + flag);
        if (flag == 1) {
            synchronized (o1) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2) {
                    System.out.println("线程1成功拿到两把锁");
                }
            }
        }
        if (flag == 0) {
            synchronized (o2) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1) {
                    System.out.println("线程2成功拿到两把锁");
                }
            }
        }
    }

    public static void main(String[] args) {
        MustDeadLock r1 = new MustDeadLock();
        MustDeadLock r2 = new MustDeadLock();
        r1.flag = 1;
        r2.flag = 0;
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
    }
}
  • 当类的对象flag = 1时(T1),先锁定O1,睡眠500毫秒,然后锁定O2;

  • 而T1在睡眠的时候另一个flag = 0的对象(T2)线程启动,先锁定O2,睡眠500毫秒,等待T1释放O1;

  • T2睡眠结束后需要锁定O1才能继续执行,而此时O1已被T1锁定;

  • T1、T2相互等待,都需要对方锁定的资源才能继续执行,从而死锁。

2、实际生产中的例子:转账

  • 需要两把锁
  • 获取两把锁成功,且余额大于0,则扣除转出人,增加收款人的余额,是原子操作。
  • 顺序相反导致死锁

代码

/**
 * 描述: 转账的时候遇到死锁,一旦打开注释,便会发生死锁
 */
public class TransferMoney implements Runnable{
    int flag = 1;
    static Account a = new Account(500);
    static Account b = new Account(500);

    @Override
    public void run() {
        if (flag == 1) {
            //a给b转200块
            transferMoney(a, b, 200);
        }
        if (flag == 0) {
            //b转给a200块
            transferMoney(b, a, 200);
        }
    }

    public static void transferMoney(Account from, Account to, int amount) {
        //拿到两把锁
        synchronized (from) {
            //try {
            //    Thread.sleep(500);
            //} catch (InterruptedException e) {
            //    e.printStackTrace();
            //}
            synchronized (to) {
                if (from.balance - amount < 0) {
                    System.out.println("余额不足,转账失败。");
                }
                from.balance -= amount;
                to.balance += amount;
                System.out.println("成功转账" + amount + "元");
            }
        }
    }
    static class Account {
        int balance;//余额

        public Account(int balance) {
            this.balance = balance;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TransferMoney r1 = new TransferMoney();
        TransferMoney r2 = new TransferMoney();
        r1.flag = 1;
        r2.flag = 0;
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("a的余额" + a.balance);
        System.out.println("b的余额" + b.balance);
    }
}

加上注释之后,一个线程获得两把锁,运行结果

image-20220204222345986

但是如果去掉注释之后,线程先获取一把锁,然后等待另一把锁,另外一个线程获取锁的顺序与之相反,这样就造成了死锁。

3、模拟多人随机转账:循环死锁

代码

package deadlock;

import deadlock.TransferMoney.Account;
import java.util.Random;

public class MultiTransferMoney {
    //账户数量
    private static final int NUM_ACCOUNTS = 500;
    //账户金额
    private static final int NUM_MONEY = 1000;
    //转账次数
    private static final int NUM_ITERATIONS = 1000000;
    //同时执行的线程数
    private static final int NUM_THREADS = 20;
    public static void main(String[] args) {
        
        Random rnd = new Random();
        Account[] accounts = new Account[NUM_ACCOUNTS];
        for (int i = 0; i < accounts.length; i++) {
            accounts[i] = new Account(NUM_MONEY);
        }

        class TransferThread extends Thread {
            @Override
            public void run() {
                for (int i = 0; i < NUM_ITERATIONS; i++) {
                    int fromAcct = rnd.nextInt(NUM_ACCOUNTS);
                    int toAcct = rnd.nextInt(NUM_ACCOUNTS);
                    int amount = rnd.nextInt(NUM_MONEY);
                    TransferMoney.transferMoney(accounts[fromAcct], accounts[toAcct], amount);
                }
            }
        }

        for (int i = 0; i < NUM_THREADS; i++) {
            new TransferThread().start();
        }
    }
}

基于2的代码加以演进。发生了循环死锁的问题。

  • 5万人很多,但是依然会发生死锁,墨菲定律
  • 发生死锁的几率不高,但是危害极大。

三、死锁发生的四个必要条件

  • 互斥条件
    • 一个资源每次只能被同一个进程或者线程使用,比如说锁。
    • 如果说一个资源是无限共享的,那么就不满足互斥条件
  • 请求与保持条件
    • 比如说第一个线程去请求第二把锁,但是我又保持有我的第一把锁。
    • 而此时,发生请求的时候,我自身阻塞了,所以我对于我已经获取的资源不变,不释放锁。
  • 不剥夺条件
    • 不能由外界来干扰,剥夺线程的锁。
  • 循环等待条件
    • 两个线程就是你等我,我等你。
    • 多个线程就是构成环路,发生死锁。

缺一不可,同时满足。

四、如何定位死锁

1、jps、jstack应用

image-20220204232843985

image-20220204232912452

结果

image-20220204232924741

2、ThreadMXBean代码演示

package deadlock;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

/**
 * 描述:     用ThreadMXBean检测死锁
 */
public class ThreadMXBeanDetection implements Runnable {

    int flag = 1;

    static Object o1 = new Object();
    static Object o2 = new Object();

    public static void main(String[] args) throws InterruptedException {
        ThreadMXBeanDetection r1 = new ThreadMXBeanDetection();
        ThreadMXBeanDetection r2 = new ThreadMXBeanDetection();
        r1.flag = 1;
        r2.flag = 0;
        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());
            }
        }
    }

    @Override
    public void run() {
        System.out.println("flag = " + flag);
        if (flag == 1) {
            synchronized (o1) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2) {
                    System.out.println("线程1成功拿到两把锁");
                }
            }
        }
        if (flag == 0) {
            synchronized (o2) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1) {
                    System.out.println("线程2成功拿到两把锁");
                }
            }
        }
    }
}

image-20220204233450005

五、修复死锁的策略

1、线上发生思索应该怎么办?

  • 线上问题都要防患于未然,不造成损失地扑灭几乎已经是不可能的事情。
  • 保存案发现场然后立刻重启服务器
  • 暂时保证线上服务的安全,然后再利用刚才保存的信息,排查死锁,修改代码,重新发版。

2、常见的修复策略

  • 避免策略:
    • 哲学家就餐的换手方案、转账换序方案
  • 检测与恢复策略:
    • 一段时间检测是否有死锁,如果有就剥夺某一个资源,来打开死锁
  • 鸵鸟策略:
    • 鸵鸟遇到危险的时候,通常就会把头埋在地上,这样一来它就看不到危险了。而鸵鸟策略的意思就是说,如果我们发生死锁的概率极其低,那么我们就直接忽略它,知道死锁发生的时候,再人工修复。

3、死锁避免策略

  • 思路:避免相反的获取锁的顺序
    • 转账的时候避免死锁
    • 实际上不在乎获取锁的顺序

转账代码修改

  • 通过hashCode来决定获取锁的顺序、冲突时需要“加时赛”
/**
 * 描述:     转账时候遇到死锁,一旦打开注释,便会发生死锁
 */
public class TransferMoney implements Runnable {

    int flag = 1;
    static Account a = new Account(500);
    static Account b = new Account(500);
    static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        TransferMoney r1 = new TransferMoney();
        TransferMoney r2 = new TransferMoney();
        r1.flag = 1;
        r2.flag = 0;
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("a的余额" + a.balance);
        System.out.println("b的余额" + b.balance);
    }

    @Override
    public void run() {
        if (flag == 1) {
            transferMoney(a, b, 200);
        }
        if (flag == 0) {
            transferMoney(b, a, 200);
        }
    }

    public static void transferMoney(Account from, Account to, int amount) {
        class Helper {

            public void transfer() {
                if (from.balance - amount < 0) {
                    System.out.println("余额不足,转账失败。");
                    return;
                }
                from.balance -= amount;
                to.balance = to.balance + amount;
                System.out.println("成功转账" + amount + "元");
            }
        }
        int fromHash = System.identityHashCode(from);
        int toHash = System.identityHashCode(to);
        if (fromHash < toHash) {
            synchronized (from) {
                synchronized (to) {
                    new Helper().transfer();
                }
            }
        }
        else if (fromHash > toHash) {
            synchronized (to) {
                synchronized (from) {
                    new Helper().transfer();
                }
            }
        }else  {//如果hash冲突,谁先拿到lock锁,谁先转账
            synchronized (lock) {
                synchronized (to) {
                    synchronized (from) {
                        new Helper().transfer();
                    }
                }
            }
        }

    }


    static class Account {

        public Account(int balance) {
            this.balance = balance;
        }

        int balance;

    }
  • 数据库中有主键就更方便了,可以通过主键的高低来决定获取锁的顺序。

4、哲学家就餐问题

问题描述

  • 先拿起左手的筷子
  • 然后拿起右手的筷子
  • 如果筷子被人使用了,那就等别人用完
  • 吃完后,把筷子放回原位

有死锁和资源耗尽的风险

死锁:每个哲学家都拿着左手的筷子,永远都在等右边的筷子(或者相反)

代码演示:哲学家进入死锁

/**
 * 描述: 演示哲学家就餐问题导致的死锁
 */
public class DinningPhilosophers {
    public static class Philosopher implements Runnable {
        private Object leftChopstick;
        private Object rightChopstick;

        public Philosopher(Object lestChopstick, Object rightChopstick) {
            this.leftChopstick = lestChopstick;
            this.rightChopstick = rightChopstick;
        }
        @Override
        public void run() {
            try {
                while (true) {
                    //思考
                    doAction("Thinking");
                    //拿左边的筷子
                    synchronized (leftChopstick) {
                        doAction("Picked up left chopstick");
                        synchronized (rightChopstick) {
                            doAction("Picked up right chopstick - eating");

                            doAction("Put down right chopstick");
                        }
                        doAction("Put down left chopstick");
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        private void doAction(String action) throws InterruptedException {
            System.out.println(Thread.currentThread().getName() + " " + action);
            Thread.sleep((long) Math.random() * 10);
        }
    }

    public static void main(String[] args) {
        Philosopher[] philosophers = new Philosopher[5];
        Object[] chopsticks = new Object[philosophers.length];

        //初始化筷子
        for (int i = 0; i < chopsticks.length; i++) {
            chopsticks[i] = new Object();
        }
        //初始化哲学家
        for (int i = 0; i < philosophers.length; i++) {
            Object leftChopstick = chopsticks[i];
            //防止越界,循环
            Object rightChopstick = chopsticks[(i + 1) % chopsticks.length];
            philosophers[i] = new Philosopher(leftChopstick, rightChopstick);

            new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();
        }
    }
}

结果演示:陷入死锁

image-20220205104730884

哲学家都拿起了左手边的筷子,不释放。

多种解决方案

  • 服务员检查(避免策略)
  • 改变一个哲学家拿叉子的顺序(避免策略)
  • 餐票(避免策略)
  • 领导调节(检测与恢复策略)

代码演示:解决死锁

image-20220205105051678

到了最后一个哲学家,获取筷子的顺序相反,打破环路。

5、死锁检测算法:锁的调用链路图

  • 允许发生死锁

  • 每次调用锁都记录,用一个有向图来维护

    • image-20220205105257009
  • 定期检查“锁的调用链路图”中是否存在环路

  • 一旦发生死锁,采取下面的死锁恢复机制恢复:

    • 进程终止
      • 逐个终止线程,直到死锁消除。
      • 终止顺序:
        • 优先级(是前台交互还是后台处理),对于程序的重要性
        • 已占用资源、还需要的资源
        • 已经运行的时间
    • 资源抢占
      • 把已经分发出去的锁给收回来
      • 让线程回退几步,这样就不用结束整个线程,成本比较低
      • 缺点:可能同一个线程一直被抢占,造成饥饿

六、实际工程中如何避免死锁

1、设置超时时间

  • 尝试锁:Lock的tryLock(long timeout, TimeUnit unit)
    • synchronized不具备尝试获取锁的能力
    • 如果获取锁事变:打日志、发报警邮件、重启等等。

代码演示:退一步海阔天空

import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 描述: 用tryLock来避免死锁
 */
public class TryLockDeadlock implements Runnable{
    int flag = 1;
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();
    
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (flag == 1) {
                try {
                    if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
                        System.out.println("线程1获取到了锁1");
                        //模拟真实的情况
                        Thread.sleep(new Random().nextInt(1000));
                        if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
                            System.out.println("线程1获取到了锁2");
                            System.out.println("线程1成功获取到了两把锁");
                            lock2.unlock();
                            lock1.unlock();
                            break;
                        } else {
                            System.out.println("线程1尝试获取锁2失败,已经重试");
                            //很重要,一定要释放
                            lock1.unlock();
                            System.out.println("线程1放弃了锁1");
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    }else {
                        System.out.println("线程1获取锁1失败,已重试");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            if (flag == 0) {
                try {
                    if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {
                        System.out.println("线程2获取到了锁2");
                        //模拟真实的情况
                        Thread.sleep(new Random().nextInt(1000));
                        if (lock1.tryLock(3000, TimeUnit.MILLISECONDS)) {
                            System.out.println("线程2获取到了锁1");
                            System.out.println("线程2成功获取到了两把锁");
                            lock1.unlock();
                            lock2.unlock();
                            break;
                        } else {
                            System.out.println("线程2尝试获取锁1失败,已经重试");
                            //很重要,一定要释放
                            lock2.unlock();
                            System.out.println("线程2放弃了锁2");
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    }else {
                        System.out.println("线程2获取锁2失败,已重试");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        TryLockDeadlock r1 = new TryLockDeadlock();
        TryLockDeadlock r2 = new TryLockDeadlock();
        r1.flag = 1;
        r2.flag = 0;
        new Thread(r1).start();
        new Thread(r2).start();
    }
}

运行结果:

image-20220205111028797

2、多使用并发类而不是自己设计锁

image-20220205111707665

image-20220205111800388

3、其他

  • 尽量降低锁的使用粒度:用不同的锁而不是一个锁

  • 如果能使用同步代码块,就不适用同步方法:自己制定锁对象

  • 避免锁的嵌套:MustDeadLock类

  • 分配资源前先看能不能收回来:银行家算法

  • 尽量不要几个功能用同一把锁:专锁专用

七、其他活性故障

死锁是最常见的活跃性问题,除了死锁之外,还有一些类似的问题,会导致程序无法顺利执行,统称为活跃性问题。

1、活锁

①什么是活锁

image-20220205112816518

  • 虽然线程并没有阻塞,也始终在运行(所以叫做“活”锁,线程是“活”的),但是程序却得不到进展,因为线程始终重复做同样的事。
  • 如果这里死锁,那么就是这里两个人都始终一动不动,直到对方先抬头,他们之间不再说话了,只是等待。

②活锁示例代码

牛郎织女共用一个勺子吃饭,互相谦让。

import java.util.Random;
import jdk.management.resource.internal.inst.RandomAccessFileRMHooks;

/**
 * 描述:     演示活锁问题
 */
public class LiveLock {

    static class Spoon {

        private Diner owner;

        public Spoon(Diner owner) {
            this.owner = owner;
        }

        public Diner getOwner() {
            return owner;
        }

        public void setOwner(Diner owner) {
            this.owner = owner;
        }

        public synchronized void use() {
            System.out.printf("%s吃完了!", owner.name);


        }
    }

    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;
                }
                Random random = new Random();
                //加入退避算法,避免一直谦让
                if (spouse.isHungry && random.nextInt(10) < 9) {
                    System.out.println(name + ": 亲爱的" + spouse.name + "你先吃吧");
                    spoon.setOwner(spouse);
                    continue;
                }

                spoon.use();
                isHungry = false;
                System.out.println(name + ": 我吃完了");
                spoon.setOwner(spouse);

            }
        }
    }


    public static void main(String[] args) {
        //两个吃饭的人
        Diner husband = new Diner("牛郎");
        Diner wife = new Diner("织女");

        //共用一个勺子
        Spoon spoon = new Spoon(husband);

        new Thread(new Runnable() {
            @Override
            public void run() {
                husband.eatWith(spoon, wife);
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                wife.eatWith(spoon, husband);
            }
        }).start();
    }
}
  • 加入随机因素,避免活锁。

image-20220205113508042

③工程中的活锁实例:消息队列

image-20220205113713885

2、饥饿

image-20220205113909673

image-20220205113917499

八、常见面试问题

  • 手写一个必然陷入死锁的例子,生产中什么场景下会发生死锁?

  • 发生死锁必须满足哪些条件?

  • 如何定位死锁?

  • 有哪些解决死锁问题的策略?

  • 讲一讲经典的哲学家就餐问题

    • 给出多种解决方案
      • 服务员检查
      • 改变拿刀叉顺序
      • 餐票(避免策略)
      • 剥夺
  • 实际工程中如何避免死锁?

    • image-20220205114320106
    • image-20220205114333836
  • 什么是活跃性问题?

    • 活锁、饥饿和死锁有什么区别?
posted @ 2022-02-05 11:46  DarkerG  阅读(61)  评论(0编辑  收藏  举报