乐趣区

关于java:死锁的3中死法

1. 什么是死锁

在多线程环境中,多个过程能够竞争无限数量的资源。当一个过程申请资源时,如果这时没有可用资源,那么这个过程进入期待状态。有时,如果所申请的资源被其余期待过程占有,那么该期待过程有可能再也无奈扭转状态。这种状况称为 死锁

在 Java 中应用多线程,就会 有可能导致死锁 问题。死锁会让程序始终 住,不再往下执行。咱们只能通过 停止并重启 的形式来让程序从新执行。

2. 造成死锁的起因

  • 以后线程 领有其余线程须要的 资源
  • 以后线程 期待其余线程已领有 的资源
  • 都不放弃 本人领有的资源

3. 死锁的必要条件

3.1 互斥

过程要求对所调配的资源(如打印机)进行排他性管制,即在一段时间内某资源仅为一个过程所占有。此时若有其余过程申请该资源,则申请过程只能期待。

3.2 不可剥夺

过程所取得的资源在未应用结束之前,不能被其余过程强行夺走,即只能由取得该资源的过程本人来开释(只能是被动开释)。

3.3 申请与放弃

过程曾经放弃了至多一个资源,但又提出了新的资源申请,而该资源已被其余过程占有,此时申请过程被阻塞,但对本人已取得的资源放弃不放。

3.4 循环期待

是指过程产生死锁后,必然存在一个过程–资源之间的环形链,艰深讲就是你等我的资源,我等你的资源,大家始终等。

4. 死锁的分类

4.1 动态程序型死锁

线程之间造成互相期待资源的环时,就会造成程序死锁 lock-ordering deadlock,多个线程试图以不同的程序来获取雷同的锁时,容易造成程序死锁,如果所有线程以固定的程序来获取锁,就不会呈现程序死锁问题

经典案例是 LeftRightDeadlock,两个办法,别离是 leftRigth、rightLeft。如果一个线程调用 leftRight,另一个线程调用 rightLeft,且两个线程是交替执行的,就会产生死锁。

public class LeftRightDeadLock {

    // 右边锁
    private static Object left = new Object();
    // 左边锁
    private static Object right = new Object();

    /**
     * 现持有右边的锁,而后获取左边的锁
     */
    public static void leftRigth() {synchronized (left) {System.out.println("leftRigth: left lock,threadId:" + Thread.currentThread().getId());
            // 休眠减少死锁产生的概率
            sleep(100);
            synchronized (right) {System.out.println("leftRigth: right lock,threadId:" + Thread.currentThread().getId());
            }
        }
    }

    /**
     * 现持有左边的锁,而后获取右边的锁
     */
    public static void rightLeft() {synchronized (right) {System.out.println("rightLeft: right lock,threadId:" + Thread.currentThread().getId());
            // 休眠减少死锁产生的概率
            sleep(100);
            synchronized (left) {System.out.println("rightLeft: left lock,threadId:" + Thread.currentThread().getId());
            }
        }
    }

    /**
     * 休眠
     *
     * @param time
     */
    private static void sleep(long time) {
        try {Thread.sleep(time);
        } catch (InterruptedException e) {e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        // 创立一个线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        executorService.execute(() -> leftRigth());
        executorService.execute(() -> rightLeft());
        executorService.shutdown();}
}

输入:

leftRigth: left lock,threadId:12
rightLeft: right lock,threadId:13

咱们发现,12 号线程锁住了右边要向左边获取锁,13 号锁住了左边,要向右边获取锁,因为两边都不开释本人的锁,互不相让,就产生了死锁。

4.1.1 解决方案

固定加锁的程序(针对锁程序死锁)

只有替换下锁的程序,让线程来了之后先获取同一把锁,获取不到就期待,期待上一个线程开释锁再获取锁。

public static void leftRigth() {synchronized (left) {
         ...
           synchronized (right) {...}
       }
   }

   public static void rightLeft() {synchronized (left) {
         ...
           synchronized (right) {...}
       }
   }

4.2 动静锁程序型死锁

因为办法入参由内部传递而来,办法外部尽管对两个参数依照固定程序进行加锁,然而因为内部传递时程序的不可控,而产生锁程序造成的死锁,即动静锁程序死锁。

上例通知咱们,交替的获取锁会导致死锁,且锁是固定的。有时候锁的执行程序并不那么清晰,参数导致不同的执行程序。经典案例是银行账户转账,from 账户向 to 账户转账,在转账之前先获取两个账户的锁,而后开始转账,如果这是 to 账户向 from 账户转账,角色调换,也会导致锁程序死锁。

/**
 * 动静程序型死锁
 * 转账业务
 */
public class TransferMoneyDeadlock {public static void transfer(Account from, Account to, int amount) {
        // 先锁住转账的账户
        synchronized (from) {System.out.println("线程【" + Thread.currentThread().getId() + "】获取【" + from.name + "】账户锁胜利");
            // 休眠减少死锁产生的概率
            sleep(100);
            // 在锁住指标账户
            synchronized (to) {System.out.println("线程【" + Thread.currentThread().getId() + "】获取【" + to.name + "】账户锁胜利");
                if (from.balance < amount) {System.out.println("余额有余");
                    return;
                } else {from.debit(amount);
                    to.credit(amount);
                    System.out.println("线程【" + Thread.currentThread().getId() + "】从【" + from.name + "】账户转账到【" + to.name + "】账户【" + amount + "】元钱胜利");
                }
            }
        }
    }

    private static class Account {
        String name;
        int balance;

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

        void debit(int amount) {this.balance = balance - amount;}

        void credit(int amount) {this.balance = balance + amount;}
    }


    /**
     * 休眠
     *
     * @param time
     */
    private static void sleep(long time) {
        try {Thread.sleep(time);
        } catch (InterruptedException e) {e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        // 创立线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        // 创立账户 A
        Account A = new Account("A", 100);
        // 创立账户 B
        Account B = new Account("B", 200);
        //A -> B 的转账
        executorService.execute(() -> transfer(A, B, 5));
        //B -> A 的转账
        executorService.execute(() -> transfer(B, A, 10));
        executorService.shutdown();}
}

输入:

线程【12】获取【A】账户锁胜利
线程【13】获取【B】账户锁胜利

而后就没有而后了,产生了死锁,咱们发现 因为对象的调用关系,产生了相互锁住资源的问题。

4.2.1 解决方案

依据传入对象的 hashCode 硬性确定加锁程序,打消可变性,防止死锁。

package com.test.thread.deadlock;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 动静程序型死锁解决方案
 */
public class TransferMoneyDeadlock {
    /**
     * 监视器,第三把锁,为了形式 HASH 抵触
     */
    private static Object lock = new Object();

    /**
     * 咱们通过上一次得失败,明确了不能依赖参数名称简略的确定锁的程序,因为参数是
     * 具备动态性的,所以,咱们扭转一下思路,间接依据传入对象的 hashCode()大小来
     * 对锁定程序进行排序(这里要明确的是如何排序不是要害,有序才是要害)。*
     * @param from
     * @param to
     * @param amount
     */
    public static void transfer(Account from, Account to, int amount) {
        /**
         * 这里须要阐明一下为什么不应用 HashCode()因为 HashCode 办法能够被重写,* 所以,咱们无奈简略的应用父类或者以后类提供的简略的 hashCode()办法,* 所以,咱们就应用零碎提供的 identityHashCode()办法,该办法保障无论
         * 你是否重写了 hashCode 办法,都会在虚拟机层面上调用一个名为 JVM_IHashCode
         * 的办法来依据对象的存储地址来获取该对象的 hashCode(),HashCode 如果不重写
         * 的话,其实也是通过这个虚拟机层面上的办法,JVM_IHashCode()办法实现的
         * 这个办法是用 C ++ 实现的。*/
        int fromHash = System.identityHashCode(from);
        int toHash = System.identityHashCode(to);
        if (fromHash > toHash) {
            // 先锁住转账的账户
            synchronized (from) {System.out.println("线程【" + Thread.currentThread().getId() + "】获取【" + from.name + "】账户锁胜利");
                // 休眠减少死锁产生的概率
                sleep(100);
                // 在锁住指标账户
                synchronized (to) {System.out.println("线程【" + Thread.currentThread().getId() + "】获取【" + to.name + "】账户锁胜利");
                    if (from.balance < amount) {System.out.println("余额有余");
                        return;
                    } else {from.debit(amount);
                        to.credit(amount);
                        System.out.println("线程【" + Thread.currentThread().getId() + "】从【" + from.name + "】账户转账到【" + to.name + "】账户【" + amount + "】元钱胜利");
                    }
                }
            }
        } else if (fromHash < toHash) {
            // 先锁住转账的账户
            synchronized (to) {System.out.println("线程【" + Thread.currentThread().getId() + "】获取【" + from.name + "】账户锁胜利");
                // 休眠减少死锁产生的概率
                sleep(100);
                // 在锁住指标账户
                synchronized (from) {System.out.println("线程【" + Thread.currentThread().getId() + "】获取【" + to.name + "】账户锁胜利");
                    if (from.balance < amount) {System.out.println("余额有余");
                        return;
                    } else {from.debit(amount);
                        to.credit(amount);
                        System.out.println("线程【" + Thread.currentThread().getId() + "】从【" + from.name + "】账户转账到【" + to.name + "】账户【" + amount + "】元钱胜利");
                    }
                }
            }
        } else {
            // 如果传入对象的 Hash 值雷同,那就加让加第三层锁
            synchronized (lock) {
                // 先锁住转账的账户
                synchronized (from) {System.out.println("线程【" + Thread.currentThread().getId() + "】获取【" + from.name + "】账户锁胜利");
                    // 休眠减少死锁产生的概率
                    sleep(100);
                    // 在锁住指标账户
                    synchronized (to) {System.out.println("线程【" + Thread.currentThread().getId() + "】获取【" + to.name + "】账户锁胜利");
                        if (from.balance < amount) {System.out.println("余额有余");
                            return;
                        } else {from.debit(amount);
                            to.credit(amount);
                            System.out.println("线程【" + Thread.currentThread().getId() + "】从【" + from.name + "】账户转账到【" + to.name + "】账户【" + amount + "】元钱胜利");
                        }
                    }
                }
            }
        }

    }

    private static class Account {
        String name;
        int balance;

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

        void debit(int amount) {this.balance = balance - amount;}

        void credit(int amount) {this.balance = balance + amount;}
    }


    /**
     * 休眠
     *
     * @param time
     */
    private static void sleep(long time) {
        try {Thread.sleep(time);
        } catch (InterruptedException e) {e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        // 创立线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        // 创立账户 A
        Account A = new Account("A", 100);
        // 创立账户 B
        Account B = new Account("B", 200);
        //A -> B 的转账
        executorService.execute(() -> transfer(A, B, 5));
        //B -> A 的转账
        executorService.execute(() -> transfer(B, A, 10));
        executorService.shutdown();}
}

输入

线程【12】获取【A】账户锁胜利
线程【12】获取【B】账户锁胜利
线程【12】从【A】账户转账到【B】账户【5】元钱胜利
线程【13】获取【B】账户锁胜利
线程【13】获取【A】账户锁胜利
线程【13】从【B】账户转账到【A】账户【10】元钱胜利

4.3 合作对象间的死锁

在合作对象之间可能存在多个锁获取的状况,然而这些获取多个锁的操作并不像在 LeftRightDeadLock 或 transferMoney 中那么显著,这两个锁并不一定必须在同一个办法中被获取。如果在持有锁时调用某个内部办法,那么这就须要警觉死锁问题,因为在这个内部办法中可能会获取其余锁,或者阻塞工夫过长,导致其余线程无奈及时获取以后被持有的锁。

上述两例中,在同一个办法中获取两个锁。实际上,锁并不一定在同一办法中被获取。经典案例,如出租车调度零碎。

/**
 * 合作对象间的死锁
 */
public class CoordinateDeadlock {
    /**
     * Taxi 类
     */
    static class Taxi {
        private String location;
        private String destination;
        private Dispatcher dispatcher;

        public Taxi(Dispatcher dispatcher, String destination) {
            this.dispatcher = dispatcher;
            this.destination = destination;
        }

        public synchronized String getLocation() {return this.location;}

        /**
         * 该办法先获取 Taxi 的 this 对象锁后,而后调用 Dispatcher 类的办法时,又须要获取
         * Dispatcher 类的 this 办法。*
         * @param location
         */
        public synchronized void setLocation(String location) {
            this.location = location;
            System.out.println(Thread.currentThread().getName() + "taxi set location:" + location);
            if (this.location.equals(destination)) {dispatcher.notifyAvailable(this);
            }
        }
    }

    /**
     * 调度类
     */
    static class Dispatcher {
        private Set<Taxi> taxis;
        private Set<Taxi> availableTaxis;

        public Dispatcher() {taxis = new HashSet<Taxi>();
            availableTaxis = new HashSet<Taxi>();}

        public synchronized void notifyAvailable(Taxi taxi) {System.out.println(Thread.currentThread().getName() + "notifyAvailable.");
            availableTaxis.add(taxi);
        }

        /**
         * 打印以后地位:有死锁危险
         * 持有以后锁的时候,同时调用 Taxi 的 getLocation 这个内部办法;而这个内部办法也是须要加锁的
         * reportLocation 的锁的程序与 Taxi 的 setLocation 锁的程序齐全相同
         */
        public synchronized void reportLocation() {System.out.println(Thread.currentThread().getName() + "report location.");
            for (Taxi t : taxis) {t.getLocation();
            }
        }

        public void addTaxi(Taxi taxi) {taxis.add(taxi);
        }
    }

    public static void main(String[] args) {ExecutorService executorService = Executors.newFixedThreadPool(10);
        final Dispatcher dispatcher = new Dispatcher();
        final Taxi taxi = new Taxi(dispatcher, "软件园");
        dispatcher.addTaxi(taxi);
        // 先获取 dispatcher 锁,而后是 taxi 的锁
        executorService.execute(() -> dispatcher.reportLocation());
        // 先获取 taxi 锁,而后是 dispatcher 的锁
        executorService.execute(() -> taxi.setLocation("软件园"));
        executorService.shutdown();}
}

4.3.1 解决方案

应用凋谢调用,凋谢调用指调用该办法不须要持有锁。

凋谢调用,是指在调用某个办法时不须要持有锁。凋谢调用能够防止死锁,这种代码更容易编写。上述调度算法齐全能够批改为开发调用,批改同步代码块的范畴,使其仅用于爱护那些波及共享状态的操作,防止在同步代码块中执行办法调用。批改 Dispatcher 的 reportLocation 办法:

4.3.1.1 setLocation 办法
/**
    * 凋谢调用,不持有锁期间进行内部办法调用
    *
    * @param location
    */
   public void setLocation(String location) {synchronized (this) {this.location = location;}
       System.out.println(Thread.currentThread().getName() + "taxi set location:" + location);
       if (this.location.equals(destination)) {dispatcher.notifyAvailable(this);
       }
   }
4.3.1.2 reportLocation 办法
/**
       * 同步块只蕴含对共享状态的操作代码
       */
      public synchronized void reportLocation() {System.out.println(Thread.currentThread().getName() + "report location.");
          Set<Taxi> taxisCopy;
          synchronized (this) {taxisCopy = new HashSet<Taxi>(taxis);
          }
          for (Taxi t : taxisCopy) {t.getLocation();
          }
      }

如果本文对您有帮忙,欢送 关注 点赞`,您的反对是我保持创作的能源。

转载请注明出处!

退出移动版