什么是死锁、活锁、饥饿?

声明:尊重他人劳动成果,转载请附带原文链接!学习交流,仅供参考!

1、死锁的概念以及危害

1.1 发生场景

发生在并发

多线程/多进程改善了系统资源的利用率并且还提高了系统的处理能力,但是并发也带了新的问题----->死锁

1.2 概念

死锁是指两个或者两个以上的线程在执行过程中,由于竞争系统资源而造成的一种阻塞的现象,若没有外力的作用下,它们都将无法执行下去,此时系统就处于死锁或者说系统产生了死锁。

1.3 死锁的危害

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

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

1.4 死锁的四个必要条件

  • 互斥条件

互斥条件是指,在同一时间段内,某个资源只能被一个线程占用,如果此时还有其他线程想请求该资源,那么就只能等待当前线程执行完成释放后,才能使用。

  • 请求与保持条件

请求与保持条件指当前已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它线程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。

  • 不剥夺条件

不剥夺条件指线程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

  • 循环等待条件

循环等待条件指在发生死锁时,必然存在一个线程——资源的环形链,即线程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

2、必然死锁的例子

当两个线程或者两个线程以上之间都相互持有对方所需要的系统资源,但是都不主动释放,一直互相等待,这样就导致线程之间都无法继续前行,导致程序陷入无限的阻塞。

请添加图片描述

代码演示

不同的flag取值 执行不同的线程

/**
 * @author delingw
 * @version 1.0
 */
public class DeadThread implements Runnable {
    
    
    public int flag = 1;
    private static Object lock1 = new Object();
    private static Object lock2 = new Object();

    @Override
    public void run() {
    
    
        if (flag == 1) {
    
    
            synchronized (lock1) {
    
    
                System.out.println("flag==1");
                try {
    
    
                    Thread.sleep(500);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                synchronized (lock2) {
    
    
                    System.out.println("成功的拿到了lock2");
                }
            }
        }
        if (flag == 0) {
    
    
            synchronized (lock2) {
    
    
                System.out.println("flag==0");
                try {
    
    
                    Thread.sleep(500);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                synchronized (lock1) {
    
    
                    System.out.println("成功拿到了lock1");
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
    
    
        DeadThread thread1 = new DeadThread();
        DeadThread thread2 = new DeadThread();
        thread1.flag = 1;
        thread2.flag = 0;
        Thread t1 = new Thread(thread1);
        Thread t2 = new Thread(thread2);
        t1.start();
        t2.start();
    }
}

运行结果

程序一直处死锁
在这里插入图片描述

而多个线程之间就是形成链路,相互依赖的状态。

请添加图片描述

3、死锁排除以及分析方法

3.1 jstack方法

利用快捷键 windows+R打开cmd窗口,输入jps,就可以看到我们执行的这个java程序的进程号(pid),然后在通过jstack pid(当前Java程序的进程号),就可以查看当前进程的错误信息。

查看进程号
向下拉就可以看到找到一处死锁
在这里插入图片描述

3.2 ThreadMXBean

代码展示

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
public class DeadThread implements Runnable {
    
    
    public int flag = 1;
    private static Object lock1 = new Object();
    private static Object lock2 = new Object();

    @Override
    public void run() {
    
    
        if (flag == 1) {
    
    
            synchronized (lock1) {
    
    
                System.out.println("flag==1");
                try {
    
    
                    Thread.sleep(500);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                synchronized (lock2) {
    
    
                    System.out.println("成功的拿到了lock2");
                }
            }
        }
        if (flag == 0) {
    
    
            synchronized (lock2) {
    
    
                System.out.println("flag==0");
                try {
    
    
                    Thread.sleep(500);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                synchronized (lock1) {
    
    
                    System.out.println("成功拿到了lock1");
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        DeadThread thread1 = new DeadThread();
        DeadThread thread2 = new DeadThread();
        thread1.flag = 1;
        thread2.flag = 0;
        Thread t1 = new Thread(thread1);
        Thread t2 = new Thread(thread2);
        t1.start();
        t2.start();
        Thread.sleep(1000);
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        long[] threads = threadMXBean.findDeadlockedThreads();
        if (threads != null && threads.length > 0) {
    
    
            for (int i = 0; i < threads.length; i++) {
    
    
                ThreadInfo threadInfo = threadMXBean.getThreadInfo(threads[i]);
                System.out.println("发现死锁了" + threadInfo.getThreadName());
            }
        }
    }
}

运行结果

直接在控制台输出。

在这里插入图片描述

4、死锁修复策略

4.1 避免死锁

哲学家就餐的换手方案。

  • 思路

避免相反的获取锁的顺序。比如转账时为了避免死锁,可以采用hashcode来决定获取锁的顺序,冲突时再添加其他操作。

  • 哲学家进餐问题描述

哲学家就餐问题可以这样表述,假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。餐桌中间有一大碗意大利面,每两个哲学家之间有一只餐叉。因为用一只餐叉很难吃到意大利面,所以假设哲学家必须用两只餐叉吃东西。他们只能使用自己左右手边的那两只餐叉。哲学家就餐问题有时也用米饭和筷子而不是意大利面和餐叉来描述,因为很明显,吃米饭必须用两根筷子。
哲学家从来不交谈,这就很危险,可能产生死锁,每个哲学家都拿着左手的餐叉,永远都在等右边的餐叉(或者相反)。即使没有死锁,也有可能发生资源耗尽。例如,假设规定当哲学家等待另一只餐叉超过五分钟后就放下自己手里的那一只餐叉,并且再等五分钟后进行下一次尝试。这个策略消除了死锁(系统总会进入到下一个状态),但仍然有可能发生“活锁”。如果五位哲学家在完全相同的时刻进入餐厅,并同时拿起左边的餐叉,那么这些哲学家就会等待五分钟,同时放下手中的餐叉,再等五分钟,又同时拿起这些餐叉。

  • 流程
    1.先拿起左手的筷子
    2.然后拿起右手的筷子
    3.如果筷子被使用了,等待别人用完
    4.吃完,然后把筷子放回原位(在这里不考虑卫生问题,滑稽)

代码演示

/**
 * @author delingw
 * @version 1.0
 * 哲学家问题  只做两件事 1、 思考 2、吃饭
 */
public class PhilosopherProblem {
    
    

    public static class Philosopher implements Runnable {
    
    
        private Object leftChopsticks;
        private Object rightChopsticks;

        public Philosopher(Object leftChopsticks, Object rightChopsticks) {
    
    
            this.leftChopsticks = leftChopsticks;
            this.rightChopsticks = rightChopsticks;
        }

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

        @Override
        public void run() {
    
    
            try {
    
    
                while (true) {
    
    
                    doaction(" 思考");
                    // 拿左筷子
                    synchronized (leftChopsticks) {
    
    
                        doaction("   拿起了左筷子");
                        // 拿右筷子
                        synchronized (rightChopsticks) {
    
    
                            doaction("   拿起了右筷子-----吃饭");
                            doaction("   释放了右筷子-----吃完饭了");
                        }
                        // 释放左筷子
                        doaction("   释放左筷子");
                    }
                }
            } 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 < chopsticks.length; i++) {
    
    
            chopsticks[i] = new Object();
        }
        // 初始化哲学家
        for (int i = 0; i < philosophers.length; i++) {
    
    
            // 左筷子
            Object leftChopsticks = chopsticks[i];
            // 右筷子
            Object rightChopsticks = chopsticks[(i + 1) % chopsticks.length];
            // 初始化哲学家
            philosophers[i] = new Philosopher(leftChopsticks, rightChopsticks);
            // 开启线程
            new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();
        }
    }
}

运行结果

所有哲学家都在拿到了左边的筷子,都在等待右边的筷子而陷入了死锁。

在这里插入图片描述

解决方法

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

 if (i == philosophers.length - 1) {
    
    
                philosophers[i] = new Philosopher(rightChopsticks, leftChopsticks);
            } else {
    
    
                philosophers[i] = new Philosopher(leftChopsticks, rightChopsticks);
            }

3. 餐票(避免策略) 例如:5个人给四张票

4. boss调节(检查与恢复策略)

4.2 检测与恢复策略

一段时间检测是否有死锁,如果有就剥夺某一个资源,来打开死锁。

检测算法锁的调用链路图
1. 允许发生死锁。
2. 每次调用锁都记录。
3. 定期检查锁的调用链路图中是否存在环路
4. 一旦发生死锁,就用死锁恢复机制进行恢复。
恢复方法
1. 逐个终止进程,直到死锁消除。
2. 资源抢占,把已经分发出去的锁给收回来或者让线程回退几步。

4.3 鸵鸟策略

如果我们发生死锁的几率极其低,那我们就直接忽略它,直到死锁发生的时候,我们才去解决它。

5、实际项目中如何避免死锁?

  • 设置超时时间

Lock的tryLock(long timeout, TimeUnit timeUnit),如果超时获取锁失败就进行日志打印、警告、重启等操作。

代码演示

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

/**
 * @author delingw
 * @version 1.0
 * 避免死锁 tryLock(long time, TimeUnit unit)   超时就放弃
 */
public class TryLockMethod implements Runnable {
    
    
    int flag = 1;
    // 两把锁
    public static Lock lock1 = new ReentrantLock();
    public 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)) {
    
    
                        try {
    
    
                            System.out.println("线程1获取了锁1");
                            Thread.sleep(new Random().nextInt(1000));
                            if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
    
    
                                try {
    
    
                                    System.out.println("线程1获取了锁2");
                                    System.out.println("线程1获取了两把锁");
                                    break;
                                } finally {
    
    
                                    lock2.unlock();
                                }
                            } else {
    
    
                                System.out.println("线程1获取锁2失败,已重试");
                            }
                        } finally {
    
    
                            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(800, TimeUnit.MILLISECONDS)) {
    
    
                        try {
    
    
                            System.out.println("线程2获取了锁2");
                            Thread.sleep(new Random().nextInt(1000) );
                            if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
    
    
                                try {
    
    
                                    System.out.println("线程2获取了锁1");
                                    System.out.println("线程2获取了两把锁");
                                    break;
                                } finally {
    
    
                                    lock1.unlock();
                                }
                            } else {
    
    
                                System.out.println("线程2获取锁1失败,已重试");
                            }
                        } finally {
    
    
                            lock2.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
    
    
                        System.out.println("线程2获取锁2失败,已重试");
                    }
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        }

    }

    public static void main(String[] args) {
    
    
        TryLockMethod method1 = new TryLockMethod();
        method1.flag = 1;
        TryLockMethod method2 = new TryLockMethod();
        method2.flag = 0;
        Thread t1 = new Thread(method1);
        Thread t2 = new Thread(method2);
        t1.start();
        t2.start();
    }
}

运行结果
在这里插入图片描述

  • 多用并发类而不是自己设计锁
  • 尽量降低锁的粒度:用不同的锁而不是一个锁
  • 如果能使用同步代码块,就不使用同步方法,因为同步代码块可以自己指定锁对象
  • 给你的线程起个名字,这样在debug或者在排查的时候可以减少很多工作量,框架和JDK都是遵守这个最佳实践
  • 避免锁的嵌套:例如我们上面的必然死锁
  • 分配资源前先看能不能收回来:例如操作系统中讲的银行家算法
  • 尽量不要几个功能用一把锁:可以专锁专用

6、什么是活锁?

6.1 概念

活锁是指虽然线程没有阻塞,也始终运行,但是程序得不到进展,因为线程始终重复做同样的事。一直谦让,导致资源一直在线程间跳动。

6.2 解决活锁的方法

  • 以太网的指数退避算法
  • 加入随机因素
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();
    }
}

7、什么是饥饿?

7.1 概念

当线程需要某些资源比如CPU,却始终得不到。

7.2 造成饥饿的原因

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

猜你喜欢

转载自blog.csdn.net/qq_40805639/article/details/121170572