线程活跃性问题及其解决方案

1.什么是死锁

死锁发生于并发中,当两个或更多线程互相持有对方的资源,但又不主动释放,令两个线程都无法前进,从而陷入无尽的等待之中的情况就是死锁。

例如上图两个线程互相持有对方需要的锁,但是都不肯释放持有的锁,从而陷入死锁。

在多个线程的情况下,存在 环形的依赖关系,这样就有可能发生死锁,例如上图,Thread1持有锁A想获取锁B,Thread2持有锁B想获取锁C,Thread3持有锁C想获取锁A,但是每个线程都不肯让出自己持有的锁,这样就发生了死锁。

2.死锁的影响

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

数据库中可以对事物进行检测和放弃,如果发生抢占的情况可以指定某个事务放弃,这样可以解决死锁。但是在JVM中不具备自动处理的能力

死锁发生的概率比较低,但是产生的危害比较大,在多线程并发情况下,影响的用户比较多。

死锁会导致系统整体崩溃,子系统崩溃,性能降低。并且压力测试无法发现所有潜在的死锁

3.发生死锁的例子

3.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();
                }
                System.out.println("thread1持有o1");
                synchronized (o2){
                    System.out.println("thread1持有o2");
                }
            }
        }
        if (flag == 0){
            synchronized (o2){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("thread2持有o2");
                synchronized (o1){
                    System.out.println("thread2持有o1");
                }
            }
        }
    }

    public static void main(String[] args) {
        MustDeadLock r1 = new MustDeadLock();
        MustDeadLock r2 = new MustDeadLock();
        r1.flag = 1;
        r2.flag = 0;
        Thread thread1 = new Thread(r1);
        Thread thread2 = new Thread(r2);
        thread1.start();
        thread2.start();
    }
}
复制代码

3.2 银行转账发生死锁

前提条件:需要把锁(将转账和被转账的线程锁住,保证中间不被干扰),在成功获取两把锁的情况下,且余额大于0,则扣除转账人,增加收款人的余额,是原子操作。 顺序相反导致死锁

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

    int flag = 1;
    //a和b没人都有500
    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 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){
            //a向b转200
            transferMoney(a, b, 200);
        }
        if (flag == 0){
            //b向a转200

            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;
        }
    }
}
复制代码

如果注释掉sleep()就不会发生死锁,但是添加了sleep()在这500ms中就会发生死锁。

3.3 模拟多人转账

/**
 * 多人转账情况下发生死锁
 */
public class MultiTransferMoney {

    //50个账户
    private static final int NUM_ACCOUNTS = 50;
    //每个账户有1000元
    private static final int NUM_MONEYS = 1000;
    //转账次数
    private static final int NUM_ITERATIONS = 1000000;
    //操作账户的人数
    private static final int NUM_THREADS = 20;

    public static void main(String[] args) {
        Random random = new Random();
        TransferMoney.Account[] accounts = new TransferMoney.Account[NUM_ACCOUNTS];
        for (int i = 0; i < accounts.length; i++) {
            accounts[i] = new TransferMoney.Account(NUM_MONEYS);
        }

        class TransferThread extends Thread {
            @Override
            public void run() {
                for (int i = 0; i < NUM_ITERATIONS; i++) {
                    int fromAccount = random.nextInt(NUM_ACCOUNTS);
                    int toAccount = random.nextInt(NUM_ACCOUNTS);
                    int amount = random.nextInt(NUM_MONEYS);

                    TransferMoney.transferMoney(accounts[fromAccount], accounts[toAccount], amount);
                }
            }
        }

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

死锁发生的概率随着账户数量的减少而增加

4.发生死锁的4个必要条件(缺一不可)

  • 互斥:当thread1拿到lockA后,其他线程就无法获取到lockA
  • 请求与保持:当thread1拿到lockA后,还一定要获取到lockB
  • 不可剥夺:在数据库中可以避免发生死锁是因为数据库自身可以剥夺某个事务,这样就会避免死锁,但是在Java中不可剥夺。
  • 循环等待:在两个线程中两个线程相互等待,在多个线程中每个线程首尾相接形成环路,也就是发生循环等待。

5.如何定位死锁

5.1 ThreadMXBean代码演示

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

    int flag = 1;
    static Object o1 = new Object();
    static Object o2 = new Object();

    @Override
    public void run() {
        if (flag == 1){
            synchronized (o1){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2){
                    System.out.println("t1持有o2");
                }
            }
        }

        if (flag == 0){
            synchronized (o2){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1){
                    System.out.println("t2持有o1");
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadMXBeanDetection d1 = new ThreadMXBeanDetection();
        ThreadMXBeanDetection d2 = new ThreadMXBeanDetection();

        d1.flag = 1;
        d2.flag = 0;

        Thread t1 = new Thread(d1);
        Thread t2 = new Thread(d2);
        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++) {
                //通过死锁的线程ID获取线程信息
                ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);
                System.out.println("发现死锁" + threadInfo.getThreadName());
            }
        }
    }
}
复制代码

发现死锁后可以通过报警或者日志的方式对死锁进行修复。

6.如何修复死锁

6.1 线上发生死锁怎么办

对于线上问题一定要防患于未然,因为在线上想要没有任何损失的修复死锁几乎是不可能的了。所以需要

  • 先将“案发现场”保存下来然后立刻重启服务器。
  • 暂时保证线上服务的安全,然后利用刚才留下的信息立刻定位死锁,进行修复,然后重新发版。

6.2 修复死锁的策略

  • 避免策略:哲学家就餐的换手方案、转账换序方案 思路:避免相反的获取锁的顺序
  • 检测与恢复策略:一段时间内检查是否发生死锁,如果发生死锁,对资源进行剥夺,从而修复死锁。
  • 鸵鸟策略:鸵鸟这种动物在遇到危险时会把头埋在地上,这样就看不到危险了。这也就是说在发生可能性低的时候可以暂时忽略掉死锁,等到发生后进行人工修复。

6.2.1 避免策略的使用

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

    int flag = 1;
    //a和b没人都有500
    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){
            //a向b转200
            transferMoney(a, b, 200);
        }
        if (flag == 0){
            //b向a转200
            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("转账失败,余额不足");
                }

                from.balance -= amount;
                to.balance += amount;
                System.out.println("成功转账:" + amount + "元");
            }
        }
        //获取转入和转出的hash值
        int fromHash = System.identityHashCode(from);
        int toHash = System.identityHashCode(to);
        if (fromHash < toHash){ //通过hash值保证了获取锁的顺序
            synchronized (from) {
                synchronized (to) {
                    new Helper().transfer();
                }
            }
        } else if (fromHash > toHash){
            synchronized (to) {
                synchronized (from) {
                    new Helper().transfer();
                }
            }
        } else {    //fromHash == toHash
            synchronized (lock){    //谁先拿到lock谁就先执行
                synchronized (to) {
                    synchronized (from) {
                        new Helper().transfer();
                    }
                }
            }
        }
    }

    //账户
    static class Account{
        int balance;

        public Account(int balance) {
            this.balance = balance;
        }
    }
}
复制代码

通过修改transfer方法计算转出和转入的hash值,通过hash值作比较来设定锁的获取顺序,这样可以避免死锁的发生。

7.哲学家就餐问题

7.1 什么是哲学家就餐问题

每个哲学家吃饭需要先拿起左手(或右手的叉子)再拿起右手(或左手)的叉子才可以吃饭,等待自己用完了放回原处,叉子再供另外的人使用(暂时不考虑卫生问题:))。

死锁:如果每个人都同时拿起了左边的叉子,这样就无法拿到右手边的叉子,这样就造成了等待的问题

/**
 * 描述:     演示哲学家就餐问题导致的死锁
 */
public class DiningPhilosophers {

    public static class Philosophers implements Runnable {

        private Object leftChopstick;
        private Object rightChopstick;

        public Philosophers(Object leftChopstick, Object rightChopstick) {
            this.leftChopstick = leftChopstick;
            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) {
        //设置哲学家的人数
        Philosophers[] philosophers = new Philosophers[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 Philosophers(leftChopstick, rightChopstick);
            new Thread(philosophers[i], "哲学家"+(i+1)+"号").start();
        }
    }
}
复制代码

导致死锁的一大特征就是:每个哲学家的左手都拿着筷子,右手无法获取筷子

7.2 解决哲学家死锁问题的4种办法

  • 服务员检查(避免策略)
  • 改变哲学家拿叉子的顺序(避免策略)
  • 餐票(将餐票数量设置为人数-1)(避免策略)
  • 领导调节(检测与恢复策略)

7.2.1 实现换手策略

public static void main(String[] args) {
        //设置哲学家的人数
        Philosophers[] philosophers = new Philosophers[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];

            if (i == philosophers.length - 1) { //让这个哲学家换手,避免形成环路
                philosophers[i] = new Philosophers(rightChopstick, leftChopstick);
            }else{
                philosophers[i] = new Philosophers(leftChopstick, rightChopstick);
            }

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

7.2.2 死锁检测与恢复策略

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

  • 允许发生死锁
  • 每次调用锁都记录
  • 定期检查锁的调用链路图是否形成环路
  • 一旦发生死锁,就用死锁恢复机制进行恢复

恢复方法1:进程终止

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

终止顺序:

  • 1.优先级(是前台交互还是后台处理)
  • 2.已占用资源,还需要的资源
  • 3.已经运行的时间

恢复方法2:资源抢占

把每个分发出去的锁收回来

让线程回退几步,这样就不用结束整个线程,成本比较低,但是这样可能会造成资源一直被抢占,造成饥饿

8.避免死锁的有效手段

8.1 设置超时时间(退一步海阔天空)

Lock的tryLock(long timeout, TimeUnit unit)

synchronized不具备尝试锁的能力

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

    int flag;
    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();

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

    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();
    }
}
复制代码

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

ConcurrentHashMap、ConcurrentLinkedQueue、AtomicBoolean等

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

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

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

8.3 降低使用锁的粒度,避免使用同一个锁

保护的范围大,效率低,容易发生死锁

8.4 尽量使用同步代码块而非同步方法

使用同步代码块相对于同步方法,缩小了保护的范围,增加了对对象的控制权,降低发生死锁的风险。

8.5 给线程起个有意义的名字

8.6 避免锁的嵌套

例如上面的MustDeadLock类

8.7 分配资源前先看能不能收回来

例如:银行家算法

8.8 专锁专用

尽量不要多个功能使用同一把锁

9.其他活性问题

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

9.1 活锁

9.1.1 什么是活锁

再回到前面的哲学家就餐问题,发生死锁是因为每个哲学家都是先拿到左手的餐具,永远在等待右手边的餐具(或者相反),这样就会发生死锁。

活锁相对与死锁更加智能一点,这些哲学家同时进入餐厅,同时拿起左边的餐具然后会等待5分钟,然后放下餐具,再等5分钟,又同时拿起餐具,这样也会导致每个哲学家无法吃饭。

换成程序中的话就是说:程序一直在运行,但是属于无用功,白白浪费资源

9.1.2 活锁的出现

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

    static class Spoon{
        private Diner onwer;

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

        public Diner getOnwer() {
            return onwer;
        }

        public void setOnwer(Diner onwer) {
            this.onwer = onwer;
        }

        public synchronized void use(){
            System.out.printf("%s has eaten!", onwer.name);
        }
    }

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

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

        public void earWith(Spoon spoon, Diner spouse){
            while (isHungry){
                //自己没有拿到勺子
                if (spoon.onwer != this){
                    //等待伴侣吃饭
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    continue;
                }

                //如果伴侣是饥饿的
                if (spouse.isHungry){
                    System.out.println(name + ": 亲爱的" + spouse.name +"你先吃吧");
                    //将勺子给伴侣
                    spoon.setOnwer(spouse);
                    continue;
                }

                //我可以吃饭了
                spoon.use();
                //吃完了改变hungry的状态
                isHungry = false;
                System.out.println(name + ": 我吃完了");
                //将勺子给伴侣
                spoon.setOnwer(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.earWith(spoon, wife);
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                wife.earWith(spoon, husband);
            }
        }).start();

    }
}
复制代码

9.1.3 如何解决活锁问题

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

解决: 加入随机因素

9.1.4 代码演示

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

    static class Spoon{
        private Diner onwer;

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

        public Diner getOnwer() {
            return onwer;
        }

        public void setOnwer(Diner onwer) {
            this.onwer = onwer;
        }

        public synchronized void use(){
            System.out.printf("%s has eaten!", onwer.name);
        }
    }

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

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

        public void earWith(Spoon spoon, Diner spouse){
            while (isHungry){
                //自己没有拿到勺子
                if (spoon.onwer != 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.setOnwer(spouse);
                    continue;
                }

                //我可以吃饭了
                spoon.use();
                //吃完了改变hungry的状态
                isHungry = false;
                System.out.println(name + ": 我吃完了");
                //将勺子给伴侣
                spoon.setOnwer(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.earWith(spoon, wife);
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                wife.earWith(spoon, husband);
            }
        }).start();

    }
}
复制代码

9.2 饥饿

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

饥饿的原因

  • 当某个线程的执行优先级过低,始终得不到CPU资源
  • 某个线程一直持有锁,却从不释放锁
  • 某程序始终占用某文件的写锁。

饥饿的危害

饥饿可能会导致响应性变差:例如一个线程负责前台的响应,另一条线程负责后台的数据处理,但是由于前台线程优先级比较低始终得不到执行,这样会导致用户体验变差。

猜你喜欢

转载自juejin.im/post/5d9f17fe5188253ec22447a9
今日推荐