多线程之死锁就是这么简单

前言

只有光头才能变强

回顾前面:html

本篇主要是讲解死锁,这是我在多线程的最后一篇了。主要将多线程的基础过一遍,之后有机会再继续深刻java

死锁是在多线程中也是比较重要的知识点了!编程

那么接下来就开始吧,若是文章有错误的地方请你们多多包涵,不吝在评论区指正哦~c#

声明:本文使用JDK1.8

1、死锁讲解

在Java中使用多线程,就会有可能致使死锁问题。死锁会让对应产生死锁的线程卡住,再也不程序往下执行。咱们只能经过停止并重启的方式来让程序从新执行。微信

  • 这是咱们很是不肯意看到的一种现象,咱们要尽量避免死锁的状况发生!

形成死锁的缘由能够归纳成三句话:多线程

  • 当前线程拥有其余线程须要的资源
  • 当前线程等待其余线程已拥有的资源
  • 都不放弃本身拥有的资源

1.1锁顺序死锁

首先咱们来看一下最简单的死锁(锁顺序死锁)是怎么样发生的:并发

public class LeftRightDeadlock {
    private final Object left = new Object();
    private final Object right = new Object();

    public void leftRight() {
        // 获得left锁
        synchronized (left) {
            // 获得right锁
            synchronized (right) {
                doSomething();
            }
        }
    }

    public void rightLeft() {
        // 获得right锁
        synchronized (right) {
            // 获得left锁
            synchronized (left) {
                doSomethingElse();
            }
        }
    }
}

咱们的线程是交错执行的,那么就颇有可能出现如下的状况:ide

  • 线程A调用leftRight()方法,获得left锁
  • 同时线程B调用rightLeft()方法,获得right锁
  • 线程A和线程B都继续执行,此时线程A须要right锁才能继续往下执行。此时线程B须要left锁才能继续往下执行。
  • 可是:线程A的left锁并无释放,线程B的right锁也没有释放
  • 因此他们都只能等待,而这种等待是无期限的-->永久等待-->死锁

1.2动态锁顺序死锁

咱们看一下下面的例子,你认为会发生死锁吗?工具

// 转帐
    public static void transferMoney(Account fromAccount,
                                     Account toAccount,
                                     DollarAmount amount)
            throws InsufficientFundsException {

        // 锁定汇帐帐户
        synchronized (fromAccount) {
            // 锁定来帐帐户
            synchronized (toAccount) {

                // 判余额是否大于0
                if (fromAccount.getBalance().compareTo(amount) < 0) {
                    throw new InsufficientFundsException();
                } else {

                    // 汇帐帐户减钱
                    fromAccount.debit(amount);

                    // 来帐帐户增钱
                    toAccount.credit(amount);
                }
            }
        }
    }

上面的代码看起来是没有问题的:锁定两个帐户来判断余额是否充足才进行转帐!oop

可是,一样有可能会发生死锁

  • 若是两个线程同时调用transferMoney()
  • 线程A从X帐户向Y帐户转帐
  • 线程B从帐户Y向帐户X转帐
  • 那么就会发生死锁。
A:transferMoney(myAccount,yourAccount,10);


B:transferMoney(yourAccount,myAccount,20);

1.3协做对象之间发生死锁

咱们来看一下下面的例子:

public class CooperatingDeadlock {
    // Warning: deadlock-prone!
    class Taxi {
        @GuardedBy("this") private Point location, destination;
        private final Dispatcher dispatcher;

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

        public synchronized Point getLocation() {
            return location;
        }

        // setLocation 须要Taxi内置锁
        public synchronized void setLocation(Point location) {
            this.location = location;
            if (location.equals(destination))
                // 调用notifyAvailable()须要Dispatcher内置锁
                dispatcher.notifyAvailable(this);
        }

        public synchronized Point getDestination() {
            return destination;
        }

        public synchronized void setDestination(Point destination) {
            this.destination = destination;
        }
    }

    class Dispatcher {
        @GuardedBy("this") private final Set<Taxi> taxis;
        @GuardedBy("this") private final Set<Taxi> availableTaxis;

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

        public synchronized void notifyAvailable(Taxi taxi) {
            availableTaxis.add(taxi);
        }

        // 调用getImage()须要Dispatcher内置锁
        public synchronized Image getImage() {
            Image image = new Image();
            for (Taxi t : taxis)
                // 调用getLocation()须要Taxi内置锁
                image.drawMarker(t.getLocation());
            return image;
        }
    }

    class Image {
        public void drawMarker(Point p) {
        }
    }
}

上面的getImage()setLocation(Point location)都须要获取两个锁的

  • 而且在操做途中是没有释放锁的

这就是隐式获取两个锁(对象之间协做)..

这种方式也很容易就形成死锁.....

2、避免死锁的方法

避免死锁能够归纳成三种方法:

  • 固定加锁的顺序(针对锁顺序死锁)
  • 开放调用(针对对象之间协做形成的死锁)
  • 使用定时锁-->tryLock()

    • 若是等待获取锁时间超时,则抛出异常而不是一直等待

2.1固定锁顺序避免死锁

上面transferMoney()发生死锁的缘由是由于加锁顺序不一致而出现的~

  • 正如书上所说的:若是全部线程以固定的顺序来得到锁,那么程序中就不会出现锁顺序死锁问题!

那么上面的例子咱们就能够改造成这样子:

public class InduceLockOrder {

    // 额外的锁、避免两个对象hash值相等的状况(即便不多)
    private static final Object tieLock = new Object();

    public void transferMoney(final Account fromAcct,
                              final Account toAcct,
                              final DollarAmount amount)
            throws InsufficientFundsException {
        class Helper {
            public void transfer() throws InsufficientFundsException {
                if (fromAcct.getBalance().compareTo(amount) < 0)
                    throw new InsufficientFundsException();
                else {
                    fromAcct.debit(amount);
                    toAcct.credit(amount);
                }
            }
        }
        // 获得锁的hash值
        int fromHash = System.identityHashCode(fromAcct);
        int toHash = System.identityHashCode(toAcct);

        // 根据hash值来上锁
        if (fromHash < toHash) {
            synchronized (fromAcct) {
                synchronized (toAcct) {
                    new Helper().transfer();
                }
            }

        } else if (fromHash > toHash) {// 根据hash值来上锁
            synchronized (toAcct) {
                synchronized (fromAcct) {
                    new Helper().transfer();
                }
            }
        } else {// 额外的锁、避免两个对象hash值相等的状况(即便不多)
            synchronized (tieLock) {
                synchronized (fromAcct) {
                    synchronized (toAcct) {
                        new Helper().transfer();
                    }
                }
            }
        }
    }
}

获得对应的hash值来固定加锁的顺序,这样咱们就不会发生死锁的问题了!

2.2开放调用避免死锁

在协做对象之间发生死锁的例子中,主要是由于在调用某个方法时就须要持有锁,而且在方法内部也调用了其余带锁的方法!

  • 若是在调用某个方法时不须要持有锁,那么这种调用被称为开放调用

咱们能够这样来改造:

  • 同步代码块最好仅被用于保护那些涉及共享状态的操做
class CooperatingNoDeadlock {
    @ThreadSafe
    class Taxi {
        @GuardedBy("this") private Point location, destination;
        private final Dispatcher dispatcher;

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

        public synchronized Point getLocation() {
            return location;
        }

        public synchronized void setLocation(Point location) {
            boolean reachedDestination;

            // 加Taxi内置锁
            synchronized (this) {
                this.location = location;
                reachedDestination = location.equals(destination);
            }
            // 执行同步代码块后完毕,释放锁



            if (reachedDestination)
                // 加Dispatcher内置锁
                dispatcher.notifyAvailable(this);
        }

        public synchronized Point getDestination() {
            return destination;
        }

        public synchronized void setDestination(Point destination) {
            this.destination = destination;
        }
    }

    @ThreadSafe
    class Dispatcher {
        @GuardedBy("this") private final Set<Taxi> taxis;
        @GuardedBy("this") private final Set<Taxi> availableTaxis;

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

        public synchronized void notifyAvailable(Taxi taxi) {
            availableTaxis.add(taxi);
        }

        public Image getImage() {
            Set<Taxi> copy;

            // Dispatcher内置锁
            synchronized (this) {
                copy = new HashSet<Taxi>(taxis);
            }
            // 执行同步代码块后完毕,释放锁

            Image image = new Image();
            for (Taxi t : copy)
                // 加Taix内置锁
                image.drawMarker(t.getLocation());
            return image;
        }
    }

    class Image {
        public void drawMarker(Point p) {
        }
    }

}

使用开放调用是很是好的一种方式,应该尽可能使用它~

2.3使用定时锁

使用显式Lock锁,在获取锁时使用tryLock()方法。当等待超过期限的时候,tryLock()不会一直等待,而是返回错误信息。

使用tryLock()可以有效避免死锁问题~~

2.4死锁检测

虽然形成死锁的缘由是由于咱们设计得不够好,可是可能写代码的时候不知道哪里发生了死锁。

JDK提供了两种方式来给咱们检测:

  • JconsoleJDK自带的图形化界面工具,使用JDK给咱们的的工具JConsole
  • Jstack是JDK自带的命令行工具,主要用于线程Dump分析。

具体可参考:

3、总结

发生死锁的缘由主要因为:

  • 线程之间交错执行

    • 解决:以固定的顺序加锁
  • 执行某方法时就须要持有锁,且不释放

    • 解决:缩减同步代码块范围,最好仅操做共享变量时才加锁
  • 永久等待

    • 解决:使用tryLock()定时锁,超过期限则返回错误信息

在操做系统层面上看待死锁问题(这是我以前作的笔记、很浅显):

参考资料:

  • 《Java核心技术卷一》
  • 《Java并发编程实战》
  • 《计算机操做系统 汤小丹》
若是文章有错的地方欢迎指正,你们互相交流。习惯在微信看技术文章,想要获取更多的Java资源的同窗,能够 关注微信公众号:Java3y。为了你们方便,刚新建了一下 qq群:742919422,你们也能够去交流交流。谢谢支持了!但愿能多介绍给其余有须要的朋友

文章的目录导航

相关文章
相关标签/搜索