1. 什么是死锁?

发生在并发中

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

image-20221103143616144

如果多个线程之间的依赖关系是环形,存在环路的锁的依赖关系,那么也可能会发生死锁

image-20221103143816233

2. 死锁的影响

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

数据库中:检测并放弃事务

JVM中:无法自动处理

几率不高但是危害大

不一定发生,但是遵守“墨菲定律

一旦发生,多是高并发场景,影响用户多

整个系统崩溃、子系统崩溃、性能降低

压力测试无法找出所有潜在的死锁

3. 发生死锁的例子

3.1 最简单的情况

两个线程持有对方的锁

/**
 * 最简单的情况 
 *  线程一获取锁一去拿锁二,但是锁二也在等锁一
 */
public class Deadlock {

    private static Object object1 = new Object();
    private static Object object2 = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object1) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "获取了锁");
                    synchronized (object2) {
                        System.out.println(Thread.currentThread().getName() + "获取了锁");
                    }
                }

            }
        },"线程一");
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object2) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "获取了锁");
                    synchronized (object1) {
                        System.out.println(Thread.currentThread().getName() + "获取了锁");
                    }
                }

            }
        },"线程二");

        t1.start();
        t2.start();

    }
}

3.2 银行转账

**
 * 描述:     转账时候遇到死锁,一旦打开注释,便会发生死锁
 */
public class TransferMoney implements Runnable {

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

    public static void main(String[] args) throws InterruptedException {
        TransferMoney t1 = new TransferMoney();
        TransferMoney t2 = new TransferMoney();
        t1.flag = 1;
        t2.flag = 0;
        Thread thread1 = new Thread(t1);
        Thread thread2 = new Thread(t2);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.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 void transferMoney(Account from, Account to, int amount) {
        synchronized (from) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (to) {
                if (from.balance - amount < 0) {
                    System.out.println("余额不足转账失败!");
                }
                from.balance -= amount;
                from.balance += amount;
                System.out.println("成功转账" + amount + "元");
            }
        }
    }


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

3.3 模拟多人随机转账

/**
 * 描述:     多人同时转账,依然很危险
 */
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();
        TransferMoney.Account[] accounts = new TransferMoney.Account[NUM_ACCOUNTS];
        for (int i = 0; i < accounts.length; i++) {
            accounts[i] = new TransferMoney.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);
                }
                System.out.println("运行结束");
            }
        }
        for (int i = 0; i < NUM_THREADS; i++) {
            new TransferThread().start();
        }
    }
}

4. 死锁的四个必要条件

  1. 互斥条件

就是说一个资源每一次只能被同一个进程或者线程使用,比如说我这里是一把锁,那么你拿到锁之后,别的线程的就不能再用这个锁了叫互斥条件,如果说这个资源可以无限共享,那么这个东西就不是互斥的,那么这种情况下,是不会发生死锁的。

  1. 请求与保持条件

第一个线程去请求第二把锁,我又保持我的第一把锁,请求的时候阻塞了对于我已经获取的那个资源我就保持不变,我也不释放

  1. 不剥夺条件

是不能有一个外界来干扰,不能有一个外界来剥夺的。所以这就叫做不剥夺条件

  1. 循环等待条件

两个两个的循环等待,其实就是你等我,我等你

而多个线程的循环等待,就是从头开始一个一个一个最终呢,是头尾相接这么一个等待的关系,这种呢就叫做循环

等待,如果你不是循环等待,也就是说你们之间不构成环路的话,那么其实呢,这并不是死锁,他们是可以解开的

以上的,这四个条件啊,我们看到都是必要条件,它不是说充分条件,它是必要条件,必要条件就是说你这四个必须同时满足缺一不可。

5. 如何定位死锁

5.1 命令行

进入bin目录

查看pid :jstack -lm

运行:jstack [pid]

image-20221103172143039

5.2ThreadMXBean代码

ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
long[] threads = mxBean.findDeadlockedThreads();
if (threads != null && threads.length > 0) {
    for (long thread : threads) {
        ThreadInfo info = mxBean.getThreadInfo(thread);
        System.out.println("发现了死锁:" + info.getThreadName());
    }
}

6. 修复死锁

6.1 线上发生死锁应该怎么办?

线上问题都需要防患于未然,不造成损失地扑灭几乎已经是不可能

保存案发现场然后立刻重启服务器

暂时保证线上服务的安全,然后在利用刚才保存的信息,排查死锁,修改代码,重新发版

6.2 死锁避免策略

避免策略:哲学家就餐的换手方案、转账换序方案

思路:避免相反的获取锁的顺序

比如转账的时候,我获取我的锁,你获取你的锁,这样就发生了死锁。

实际上不在乎获取锁的顺序

代码演示:

通过hashcode来决定获取锁的顺序、冲突时需要“加时赛“

有主键更方便

import 死锁.TransferMoney;

public class TransferMoney1 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 {
        TransferMoney1 r1 = new TransferMoney1();
        TransferMoney1 r2 = new TransferMoney1();
        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  {
            synchronized (lock) {
                synchronized (to) {
                    synchronized (from) {
                        new Helper().transfer();
                    }
                }
            }
        }

    }


    static class Account {

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

        int balance;

    }
}

哲学家就餐–流程

image-20221105150114313

先拿起左手的筷子

然后拿起右手的筷子

如果筷子被人使用了,那就等别人用完

吃完后,把筷子放回原位

/**
 * 描述:     演示哲学家就餐问题导致的死锁
 *
 * 输出:
 * 哲学家2号 思考
 * 哲学家4号 思考
 * 哲学家1号 思考
 * 哲学家3号 思考
 * 哲学家5号 思考
 * 哲学家5号 捡起左边的筷子
 * 哲学家2号 捡起左边的筷子
 * 哲学家3号 捡起左边的筷子
 * 哲学家4号 捡起左边的筷子
 * 哲学家1号 捡起左边的筷子
 */
public class DiningPhilosophers {

    public static class Philosopher implements Runnable {

        private Object leftChopstick;
        private Object rightChopstick;

        public Philosopher(Object leftChopstick, Object rightChopstick) {
            this.leftChopstick = leftChopstick;
            this.rightChopstick = rightChopstick;
        }



        @Override
        public void run() {
            try {
                while (true) {
                    doAction("思考");
                    synchronized (leftChopstick) {
                        doAction("捡起左边的筷子");
                        synchronized (rightChopstick) {
                            doAction("拿起正确的筷子-吃");
                            doAction("放下右边的筷子");
                        }
                        doAction("放下左边的筷子");
                    }
                }
            } 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(rightChopstick, leftChopstick);

                philosophers[i] = new Philosopher(leftChopstick, rightChopstick);

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

服务员检查(避免策略)
改变一个哲学家拿叉子的顺序(避免策略)
餐票(避免策略)

//如果是最后一个叫他先那右边的
 if (i==chopsticks.length-1){
     philosophers[i] = new Philosopher(rightChopstick,leftChopstick);
 }else {
     philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
 }

6.3 死锁检测与恢复策略

检测与恢复策略:一段时间检测是否有死锁,如果有就剥夺某一个资源,来打开死锁

领导调节(检测与恢复策略)

死锁检测算法

允许发生死锁

每次调用锁都记录

定期检查锁的调用链路图”中是否存在环路

一旦发生死锁,就用死锁恢复机制进行恢复

恢复方法1:进程终止

逐个终止线程,直到死锁消除。

终止顺序
优先级(是前台交互还是后台处理)
已占用资源、还需要的资源
已经运行时间

恢复方法2:资源抢占

把已经分发出去的锁给收回来
让线程回退几步,这样就不用结束整个线程,成本比较低
缺点:可能同一个线程一直被抢占,那就造成饥饿

6.4 蛇鸟策略(不推荐)

蛇鸟策略:蛇鸟这种动物在遇到危险的时候,通常就会把头埋在地上,这样一来它就看不到危险了。而蛇鸟策略的意思就是说,如果我们发生死锁的概率极其低,那么我们就直接忽略它,直到死锁发生的时候,再人工修复。

7. 实际工程中避免死锁的8个tips

7.1 设置超时时间

使用:Lock的tryLock(long timeout, TimeUnit unit)

因为synchronized不具备尝试锁的能力

造成超时的可能性多:发生了死锁、线程陷入死循环、线程执行很慢

获取锁失败打日志、发报警邮件、重启等

代码演示

线程有获取了锁1,休眠,然后在休眠的过程中,线程2也获取了锁2,此时线程1获取不到锁2,然后线程1就会释放此时线程2就获取了两把锁,此时线程2释放了,线程1也就可以获取两把锁了。

/**
 * 描述:用tryLock来避免死锁
 *
 */
public class TryLockDeadlock implements Runnable {

    int flag = 1;
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();

    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();
    }

    @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();
                            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();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("线程2获取锁2失败,已重试");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

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

ConcurrentHashMap, ConcurrentLinkedQueue,AtomicBoolean等

实际应用中java.util.concurrent.atomic十分有用,简单方便且效率比使用Lock更高

多用并发集合少用同步集合,并发集合比同步集合的可扩展性更好

并发场景需要用到map,首先想到用ConcurrentHashMap

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

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

7.5 给你的线程起个有意义的名字: debug和排查时事半功倍,框架和JDK都遵守这个最佳实践

7.6 避免锁的嵌套MustDeadLock类

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

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

8. 活锁

活锁哲学家问题:在完全相同的时刻进入餐厅,并同时拿起左边的餐叉那么这些哲学家就会等待五分钟,同时放下手中的餐叉,再等五分钟,又同时拿起这些餐叉

虽然线程并没有阻塞,也始终在运行(所以叫做“活”锁,线程是“活”的),但是程序却得不到进展,因为线程始终重复做同样的事

如果这里死锁,那么就是这里两个人都始终一动不动,直到对方先抬头,他们之间不再说话了,只是等待

8.1 活锁演示

/**
 * 描述:     演示活锁问题
 */
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();
    }
}

8.2 解决活锁问题

原因:重试机制不变,消息队列始终重试,吃饭始终谦让

以太网的指数退避算法

加入随机因素解决

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

策略:消息如果处理失败,就放在队列开头重试

由于依赖服务出了问题处理该消息一直失败

没阻塞,但程序无法继续

解决:放到队列尾部、重试限制

image-20221106204711315

9. 饥饿

当线程需要某些资源(例如CPU),但是却始终得不到

线程的优先级设置得过于低,或者有某线程持有锁同时又无限循环从而不释放锁,或者某程序始终占用某文件的写锁

饥饿可能会导致响应性差:比如,我们的浏览器有一个线程负责处理前台响应(打开收藏夹等动作),另外的后台线程负责下载图片和文件、计算宣染等。在这种情况下,如果后台线程把CPU资源都占用了,那么前台线程将无法得到很好地执行,这会导致用户的体验很差

10. 常见面试问题

1.写一个必然死锁的例子(我面百度的时候考过),生产中什么场景下会发生死锁?

在一个方法中获取多个锁,这种是比较容易发生死锁的,但是也有不明显的情况,比如说我们不是在一个方法中获取两个锁,而是在一个方法中先获取一个锁,再去调用其他的方法但是在其他的方法中确实又获取到其他的锁了,这样一来呢相当于一个循环调用,也形成了锁的链路,也可能会造成死锁,而这些情况呢,在我们实际生产中,比如说库存的增减,比如说金钱的转移都问题。

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

一定要满足这四个条件,第一个互斥条件,我们这个资源同时只能被一个线程或者进程使用

请求与保持条件,我们这个线程请求另外的一个锁,并且手中持有一个锁,而且它还不放弃。

不剥夺条件,就是不能被剥夺我们这些线程之间,自己持有资源,但是你又不能把我的直接抢走,这样一来才有可能会陷入死锁

循环等待条件,是在多个线程中体现为只有构成环路才有可能会发生死索,而两个现场之间,它的循环等待,意味着就是你等我我等你

3.如何定位死锁?

jstack

ThreadMXBean

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

避免策略:哲学家就餐的换手方案、转账换序方案

检测与恢复策略:一段时间检测是否有死锁,如果有就剥夺某一个资源,来打开死锁

蛇鸟策略:蛇鸟这种动物在遇到危险的时候,通常就会把头埋在地上,这样一来它就看不到危险了。而蛇鸟策略的意思就是说,如果我们发生死锁的概率极其低,那么我们就直接忽略它,直到死锁发生的时候,再人工修复。

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

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

6.实际工程中如何避免死锁?

8点

7.什么是活跃性问题?活锁、饥饿和死锁有什么区别?

原文地址:http://www.cnblogs.com/mrwyk/p/16889126.html

1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长! 2. 分享目的仅供大家学习和交流,请务用于商业用途! 3. 如果你也有好源码或者教程,可以到用户中心发布,分享有积分奖励和额外收入! 4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解! 5. 如有链接无法下载、失效或广告,请联系管理员处理! 6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需! 7. 如遇到加密压缩包,默认解压密码为"gltf",如遇到无法解压的请联系管理员! 8. 因为资源和程序源码均为可复制品,所以不支持任何理由的退款兑现,请斟酌后支付下载 声明:如果标题没有注明"已测试"或者"测试可用"等字样的资源源码均未经过站长测试.特别注意没有标注的源码不保证任何可用性