资讯专栏INFORMATION COLUMN

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

winterdawn / 2452人阅读

摘要:此时线程需要锁才能继续往下执行。但是线程的锁并没有释放,线程的锁也没有释放。

前言
只有光头才能变强

回顾前面:

ThreadLocal就是这么简单

多线程三分钟就可以入个门了!

多线程基础必要知识点!看了学习多线程事半功倍

Java锁机制了解一下

AQS简简单单过一遍

Lock锁子类了解一下

线程池你真不来了解一下吗?

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

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

那么接下来就开始吧,如果文章有错误的地方请大家多多包涵,不吝在评论区指正哦~

声明:本文使用JDK1.8
一、死锁讲解

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

我们的线程是交错执行的,那么就很有可能出现以下的情况:

线程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);
                }
            }
        }
    }

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

但是,同样有可能会发生死锁

如果两个线程同时调用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 taxis;
        @GuardedBy("this") private final Set availableTaxis;

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

        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)都需要获取两个锁的

并且在操作途中是没有释放锁的

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

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

二、避免死锁的方法

避免死锁可以概括成三种方法:

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

开放调用(针对对象之间协作造成的死锁)

使用定时锁-->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 taxis;
        @GuardedBy("this") private final Set availableTaxis;

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

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

        public Image getImage() {
            Set copy;

            // Dispatcher内置锁
            synchronized (this) {
                copy = new HashSet(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分析。

具体可参考:

https://www.cnblogs.com/flyingeagle/articles/6853167.html

三、总结

发生死锁的原因主要由于:

线程之间交错执行

解决:以固定的顺序加锁

执行某方法时就需要持有锁,且不释放

解决:缩减同步代码块范围,最好仅操作共享变量时才加锁

永久等待

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

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

操作系统第五篇【死锁】

参考资料:

《Java核心技术卷一》

《Java并发编程实战》

《计算机操作系统 汤小丹》

如果文章有错的地方欢迎指正,大家互相交流。习惯在微信看技术文章,想要获取更多的Java资源的同学,可以关注微信公众号:Java3y。为了大家方便,刚新建了一下qq群:742919422,大家也可以去交流交流。谢谢支持了!希望能多介绍给其他有需要的朋友

文章的目录导航

https://zhongfucheng.bitcron.com/post/shou-ji/wen-zhang-dao-hang

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/71107.html

相关文章

  • 超详细的Java面试题总结(二)Java基础知识篇

    摘要:超详细的面试题总结一之基本知识多线程和虚拟机创建线程有几种不同的方式你喜欢哪一种为什么继承类实现接口应用程序可以使用框架来创建线程池实现接口。死亡线程方法执行结束,或者因异常退出了方法,则该线程结束生命周期。死亡的线程不可再次复生。 超详细的Java面试题总结(一)之Java基本知识 多线程和Java虚拟机 创建线程有几种不同的方式?你喜欢哪一种?为什么? 继承Thread类 实现R...

    wangjuntytl 评论0 收藏0
  • 线程编程完全指南

    摘要:在这个范围广大的并发技术领域当中多线程编程可以说是基础和核心,大多数抽象并发问题的构思与解决都是基于多线程模型来进行的。一般来说,多线程程序会面临三类问题正确性问题效率问题死锁问题。 多线程编程或者说范围更大的并发编程是一种非常复杂且容易出错的编程方式,但是我们为什么还要冒着风险艰辛地学习各种多线程编程技术、解决各种并发问题呢? 因为并发是整个分布式集群的基础,通过分布式集群不仅可以大...

    mengera88 评论0 收藏0
  • Java基础学习——线程线程间通信(安全问题、等待唤醒机制)

    摘要:线程间通信其实就是多个线程操作同一个资源,但动作不同。同步前提是多线程。将该线程载入线程池,等待唤醒。该方法抛出异常,故需要配合使用随机唤醒线程池中一线程。线程为了检测死锁,它需要递进地检测所有被请求的锁。 线程间通信 其实就是多个线程操作同一个资源,但动作不同。示例:在某个数据库中,Input输入人的姓名,性别,Output输出,两个线程同时作用。思考:1.明确哪些代码是多线程操作的...

    CocoaChina 评论0 收藏0
  • 解决死锁的100种方法

    摘要:虽然本文是一篇介绍死锁及其解决方式的文章,但是对于多线程程序中的非死锁问题我们也应该有所了解,这样才能写出正确且高效的多线程程序。 死锁是多线程编程或者说是并发编程中的一个经典问题,也是我们在实际工作中很可能会碰到的问题。相信大部分读者对死锁这个词都是略有耳闻的,但从我对后端开发岗位的面试情况来看很多同学往往对死锁都还没有系统的了解。虽然死锁听起来很高深,但是实际上已经被研究得比较透彻...

    caige 评论0 收藏0
  • (十四)java线程死锁以及解决方案

    摘要:本人邮箱欢迎转载转载请注明网址代码已经全部托管有需要的同学自行下载引言多线程如果设计的不合理的话很可能就会出现死锁当两个或者多个线程同事想要去获取共享资源的锁时但每个线程都要等其他线程把他们各自的锁给释放才能继续运行这就是死锁出现死锁必须具 本人邮箱: 欢迎转载,转载请注明网址 http://blog.csdn.net/tianshi_kcogithub: https://github...

    wendux 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<