死锁-多线程

什么是死锁?所谓死锁,是指多个线程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,多个线程都无法继续执行,总而导致死锁现象。

下面演示一种必定发生死锁的情况

package DeadLock;
/**
 * @program:多线程和IO
 * @descripton:打开注释,就会死锁。
 * @author:ZhengCheng
 * @create:2021/9/30-21:08
 **/
public class DeadLockDemo{
    static Object lock1 =new Object();
    static Object lock2 =new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new a());
        Thread t2 = new Thread(new b());
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("end");
    }
    static class a implements Runnable{
        @Override
        public void run() {
            synchronized (lock1){
                System.out.println("getLock1");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2){
                    /*try {
                        lock1.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }*/
                    System.out.println("getLock1+Lock2");
                }
            }
        }
    }
    static class b implements Runnable{
        @Override
        public void run() {
            synchronized (lock2){
                System.out.println("getLock2");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1){
                    /*try {
                        lock2.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }*/
                    System.out.println("getLock2+Lock1");
                }
            }
        }
    }
}

 上面演示的就是一种死锁的情况。死锁虽然发生的概率小,但是一旦死锁,就会出现很大的问题。我们用两个转账的案例,来模拟现实中可能出现的死锁情况。

第一种是两个人互相转账

package DeadLock;

/**
 * @program:多线程和IO
 * @descripton:转账时遇到死锁,一旦打开注释
 * @author:ZhengCheng
 * @create:2021/9/30-21:41
 **/
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 r1 = new TransferMoney();
        TransferMoney r2 = new TransferMoney();
        r1.flag = 1;
        r2.flag = 0;
        Thread thread1 = new Thread(r1);
        Thread thread2 = new Thread(r2);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(a.balance);
        System.out.println(b.balance);
        System.out.println("end");
    }
    @Override
    public void run() {
        if (flag == 1){

            transferMoney(a,b,200);
        }
        if (flag == 0){

            transferMoney(b,a,300);
        }
    }
    public static void transferMoney(Account from , Account to ,int money ){
        synchronized (from){
            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (to){
                if (from.balance - money < 0){
                    System.out.println("余额不足");
                    return;
                }else {
                    from.balance -= money;
                    to.balance += money;
                    System.out.println("交易成功,余额增加"+money);
                }
            }
        }
    }
    public static class Account{
        int balance;
        public Account(int balance) {
            this.balance = balance;
        }
    }
}

第二种是很多个人随机转账。(开启多个线程)

package DeadLock;

import DeadLock.TransferMoney.Account;

import java.util.Random;

/**
 * @program:多线程和IO
 * @descripton:几千个人,开启多个线程,看是否会发生死锁,更具有普遍性
 * @author:ZhengCheng
 * @create:2021/10/1-22:21
 **/
public class TransferManyMan {
    private static int Num_Acct = 500;
    private static int Num_Money = 1000;
    private static int Num_Trans = 1000000;
    private static int Num_Threads = 20;

    public static void main(String[] args) {
        Account[] accounts = new Account[Num_Acct];
        for (int i = 0; i < Num_Acct; i++) {
            accounts[i] = new Account(Num_Money);
        }
        Random random = new Random();
        for (int i = 0 ; i< Num_Threads ; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < Num_Trans; i++) {
                        Account from_acc = accounts[random.nextInt(Num_Acct)];
                        Account to_acc = accounts[random.nextInt(Num_Acct)];
                        TransferMoney.transferMoney(from_acc,to_acc,random.nextInt(Num_Money));
                    }
                }
            }).start();
        }
    }
}

 最后不能发现,执行上述代码,都发生了死锁现象。可见,死锁的产生,对我们日常使用以及是有极大的危害的。

        从上面是三个案例,我们可以总结一下发生死锁的几个必要条件

♦ 互斥条件        ♦ 请求与保持条件        ♦ 不剥夺条件        ♦ 循环等待条件

当死锁发生以后,我们必然会想定位死锁发生的位置。

如何定位呢?

使用jstack命令行

使用ThreadMXBean(主要)

下面使用之前的Demo,我们使用ThreadMXBean来找到发生死锁的线程。

package DeadLock;

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

/**
 * @program:多线程和IO
 * @descripton:打开注释,就会死锁。
 * @author:ZhengCheng
 * @create:2021/9/30-21:08
 **/
public class ThreadMXBeanDetection implements Runnable {
    public ThreadMXBeanDetection(int flag) {
        this.flag = flag;
    }
    int flag;
    static Object lock1 = new Object();
    static Object lock2 = new Object();

    public static void main(String[] args) throws InterruptedException {
        ThreadMXBeanDetection td1 = new ThreadMXBeanDetection(1);
        ThreadMXBeanDetection td2 = new ThreadMXBeanDetection(0);
        Thread t1 = new Thread(td1);
        Thread t2 = new Thread(td2);
        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() {
        if (flag == 1) {
            synchronized (lock1) {
                System.out.println("getLock1");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println("getLock1+Lock2");
                }
            }
        }
        if (flag == 0) {
            synchronized (lock2) {
                System.out.println("getLock2");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println("getLock2+Lock1");
                }
            }
        }
    }

}

 能够找到死锁的位置,那么我们肯定要尝试去修复死锁的问题。

修复死锁的问题有三种解决方法。

1.避免死锁(通过对代码的设计,避免死锁的发生)

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

3.鸵鸟策略

那么下面以最经典的哲学家就餐问题,来展示修复死锁的各种方法。

首先写出哲学家就餐的demo

package DeadLock;

/**
 * @program:多线程和IO
 * @descripton:哲学家就餐问题
 * @author:ZhengCheng
 * @create:2021/10/2-21:33
 **/
public class DiningTable {
    public static class Philosopher implements Runnable{
        private Object leftChop ;
        private Object rightChop;

        public Philosopher(Object leftChop, Object rightChop) {
            this.leftChop = leftChop;
            this.rightChop = rightChop;
        }

        @Override
        public void run() {
            while (true){
                try {
                    doAction("Thinking");
                    synchronized (leftChop){
                        doAction("Get left Chop");
                        synchronized (rightChop){
                            doAction("Get right chop");
                            doAction("EAT");
                            doAction("Put down right chop");
                        }
                        doAction("Put down left chop");
                    }
                } 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[] chops = new Object[philosophers.length];
        for (int i = 0; i < chops.length; i++) {
            chops[i] = new Object();
        }
        for (int i = 0; i < philosophers.length; i++) {
            Object left = chops[i];
            Object right = chops[(i+1)%philosophers.length];
            philosophers[i] = new Philosopher(left ,right);
            new Thread(philosophers[i],"哲学家"+(i+1)+"号").start();
        }

    }
}

 解决哲学家就餐问题,我们有许多种方法。按照方法同样归属于最初的三种。

避免原则:通过逻辑设计,让其规避死锁的风险。只要锁构成的有向图没有成环,那么就没有死锁的风险。于是给出一个经典的解决方案,让其中一个哲学家,换手操作。

检测与恢复策略:我们可以通过对其进行管理,在发生死锁时,强制让某人或者全部停下,释放锁。

鸵鸟原则:不推荐

换手操作只需要在最后创建哲学家对象时进行修改即可

for (int i = 0; i < philosophers.length; i++) {
    //解决哲学家问题,换手策略,让一个哲学家,换一个方向拿东西
    Object left = chops[i];
    Object right = chops[(i+1)%philosophers.length];
    if (i == philosophers.length-1){
        philosophers[i] = new Philosopher(right,left);
    }else {
        philosophers[i] = new Philosopher(left ,right);
    }
    new Thread(philosophers[i],"哲学家"+(i+1)+"号").start();
}

 那么我们在实际的开发中,如何有效的避免死锁呢?

1.设置超时时间。使用try-lock的方式。

2.多使用并发类,而不自己创建类(ConcurrentHashMap之类的类,原子类等)

3.尽量降低锁的使用粒度,尽可能使用不同的锁而不是同一个锁。

4.尽量使用同步代码块,不适用同步方法。同时使用同步代码块,方便我们控制,自己指定锁

5.新建线程时,要为线程起有意义的名字,方便debug和排查。

6.避免出现锁的嵌套

7.分配资源前能不能收回来:银行家算法

8.尽量不要几个功能使用同一把锁。


活锁(Livelock):

活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程。处于活锁的实体是在不断的改变状态,活锁有可能自行解开。但是活锁始终会处于运行状态,甚至比死锁更糟糕。

下面用一个经典的就餐问题,演示活锁的情况。当两人就餐时,只有一个勺子,两个人都十分的谦让,看到对方没有勺子,就会想让给对方。这样就会发生活锁。

package DeadLock;

import java.util.Random;

/**
 * @program:多线程和IO
 * @descripton:牛郎织女演示活锁
 * @author:ZhengCheng
 * @create:2021/10/3-15:59
 **/
public class LiveLockDemo {
    static class Spoon {
        private Owner owner;

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

        public Owner getOwner() {
            return owner;
        }

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

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

    static class Owner {
        private String name;
        private boolean isHungry;

        public Owner(String name) {
            this.name = name;
            isHungry = true;
        }

        public void eatWith(Spoon spoon, Owner lover) {
            while (isHungry) {
                if (spoon.owner != this) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    continue;
                }

                if (lover.isHungry /*&& new Random().nextInt(10) < 9*/) {
                    System.out.println(name + ":" + lover.name + "你先吃");
                    spoon.setOwner(lover);
                    continue;
                }

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

            }
        }
    }

    public static void main(String[] args) {
        Owner o1 = new Owner("a");
        Owner o2 = new Owner("b");
        Spoon spoon = new Spoon(o1);
        new Thread(new Runnable() {
            @Override
            public void run() {
                o1.eatWith(spoon,o2);
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                o2.eatWith(spoon,o1);
            }
        }).start();
    }
}

上述代码就会发生活锁的情况。两个线程互相谦让,导致没有人开始执行任务。想要解决这个问题,我们最好使用几种方法。比如设计一个谦让的规则,让其不活锁,或者我们可以增加一些随机性,而让其不是百分百谦让。上述的解决方法,只需将代码的注释激活即可。

饥饿:

饥饿 ,与死锁和活锁非常相似。是指一个可运行的进程尽管能继续执行,但被调度器无限期地忽视,而不能被调度执行的情况。有可能是因为线程的优先级设置得过低,或者有某线程始终持有锁同时有无限循环从而不释放锁,或者某程序始终占用某文件的写锁。

常见面试问题

1.写一个必然死锁的例子。

2.发生死锁必须满足哪些条件(4个) 互斥、请求与保持、不剥夺、循环等待

3.如何定位死锁

4.解决死锁的策略(3个)

5.讲一讲哲学家就餐问题

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值