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

小编 2026-07-01 阅读:464 评论:0
前言只有光头才能变强回顾前面:ThreadLocal就是这么简单多线程三分钟就可以入个门了!多线...

前言

只有光头才能变强

回顾前面:

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

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

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

声明:本文使用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<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)都需要获取两个锁的

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

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

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

二、避免死锁的方法

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

  • 固定加锁的顺序(针对锁顺序死锁)
  • 开放调用(针对对象之间协作造成的死锁)
  • 使用定时锁-->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分析。

具体可参考:

三、总结

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

  • 线程之间交错执行
    • 解决:以固定的顺序加锁
  • 执行某方法时就需要持有锁,且不释放
    • 解决:缩减同步代码块范围,最好仅操作共享变量时才加锁
  • 永久等待
    • 解决:使用tryLock()定时锁,超过时限则返回错误信息

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

参考资料:

  • 《Java核心技术卷一》
  • 《Java并发编程实战》
  • 《计算机操作系统 汤小丹》

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

文章的目录导航

更多的文章可往:文章的目录导航
版权声明

本文仅代表作者观点,不代表百度立场。
本文系作者授权百度百家发表,未经许可,不得转载。

热门文章
  • 机房智能化温湿度解决方式之POE供电以太网温湿度传感器

    机房智能化温湿度解决方式之POE供电以太网温湿度传感器
    机房智能化温湿度解决方式之POE供电以太网温湿度传感器 北京盈创力和电子科技有限公司 智能型TCP网口温湿度记录仪 北京IP网络温湿度记录仪厂家,北京盈创力和 北京智能型TCP网口温湿度记录仪IP网络温湿度记录仪是一种新型的基于TCP/IP协议双绞线以太网标准温湿度采集模块,利用它可以实现现场温度值、相对湿度值的采集,同时利用其自身的RJ45通信接口可以方便地和机房监控主机或交换机集线器进行联网。 工作于-40℃~85℃工业级带...
  • Sequential Monte Carlo Methods (SMC) 序列蒙特卡洛/粒子滤波/Bootstrap Filtering

    Sequential Monte Carlo Methods (SMC) 序列蒙特卡洛/粒子滤波/Bootstrap Filtering
    Problem Statement 我们考虑一个具有马尔可夫性质、非线性、非高斯的状态空间模型(State Space Model):对于一个时间序列上的观测结果{yt,t∈N}\\{ y_t , t \\in N \\}{yt​,t∈N},我们认为每个观测结果yty_tyt​的生成依赖于一个无法直接观察的隐变量xt∈{xt,t∈N}x_t \\in \\{x_t , t \\in N \\}xt​∈{xt​,t∈N},即:p(...
  • HTTP状态保持的原理

    HTTP状态保持的原理
    a)在用户登录之后,浏览器返回响应的时候会在响应中添加上cookieb)浏览器接收到cookie之后会自动保存c)当用户再次请求同一服务器中的其他网页的时候,浏览器会自动带上之前保存的cookied)服务接收到请求之后可以请 request 对象中取到cookie 判断当前用户是否登录  Http是无状态的,就是连接时数据互通,关闭后...
  • Hive 系统函数及示例

    Hive 系统函数及示例
    查看所有系统函数 show functions; 函数分类 内置函数【系统函数】 数学函数: floor、round、ceil、cos、log2等 字符串函数: length、reverse、trim、lower、get_json_object、repeat等 收集函数: size 转换函数: cast 日期函数: year、month、datediff、date、date_add等 条件函数: coalesce、case…w...
  • CSRF的原理和防范措施

    CSRF的原理和防范措施
    a)攻击原理:i.用户C访问正常网站A时进行登录,浏览器保存A的cookieii.用户C再访问攻击网站B,网站B上有某个隐藏的链接或者图片标签会自动请求网站A的URL地址,例如表单提交,传指定的参数iii.而攻击网站B在访问网站A的时候,浏览器会自动带上网站A的cookieiv.所以网站A在接收到请求之后可判断当前用户是登录状态,所以...
标签列表