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

简介: 笔记

前言


回顾前面:

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锁也没有释放
  • 所以他们都只能等待,而这种等待是无期限的-->永久等待-->死锁

0.jpg


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()定时锁,超过时限则返回错误信息

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


目录
相关文章
|
11天前
|
Java 数据库连接 数据库
不同业务使用同一个线程池发生死锁的技术探讨
【10月更文挑战第6天】在并发编程中,线程池是一种常用的优化手段,用于管理和复用线程资源,减少线程的创建和销毁开销。然而,当多个不同业务场景共用同一个线程池时,可能会引发一系列并发问题,其中死锁就是最为严重的一种。本文将深入探讨不同业务使用同一个线程池发生死锁的原因、影响及解决方案,旨在帮助开发者避免此类陷阱,提升系统的稳定性和可靠性。
36 5
|
17天前
|
安全 Java 程序员
【多线程-从零开始-肆】线程安全、加锁和死锁
【多线程-从零开始-肆】线程安全、加锁和死锁
32 0
|
2月前
|
安全 算法 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(下)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
70 6
|
2月前
|
存储 安全 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(中)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
77 5
|
2月前
|
存储 安全 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(上)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
73 3
|
2月前
|
Java
Java多线程-死锁的出现和解决
死锁是指多线程程序中,两个或以上的线程在运行时因争夺资源而造成的一种僵局。每个线程都在等待其中一个线程释放资源,但由于所有线程都被阻塞,故无法继续执行,导致程序停滞。例如,两个线程各持有一把钥匙(资源),却都需要对方的钥匙才能继续,结果双方都无法前进。这种情况常因不当使用`synchronized`关键字引起,该关键字用于同步线程对特定对象的访问,确保同一时刻只有一个线程可执行特定代码块。要避免死锁,需确保不同时满足互斥、不剥夺、请求保持及循环等待四个条件。
|
2月前
|
Java 测试技术 PHP
父子任务使用不当线程池死锁怎么解决?
在Java多线程编程中,线程池有助于提升性能与资源利用效率,但若父子任务共用同一池,则可能诱发死锁。本文通过一个具体案例剖析此问题:在一个固定大小为2的线程池中,父任务直接调用`outerTask`,而`outerTask`再次使用同一线程池异步调用`innerTask`。理论上,任务应迅速完成,但实际上却超时未完成。经由`jstack`输出的线程调用栈分析发现,线程陷入等待状态,形成“死锁”。原因是子任务需待父任务完成,而父任务则需等待子任务执行完毕以释放线程,从而相互阻塞。此问题在测试环境中不易显现,常在生产环境下高并发时爆发,重启或扩容仅能暂时缓解。
|
3月前
|
消息中间件 算法 Java
(十四)深入并发之线程、进程、纤程、协程、管程与死锁、活锁、锁饥饿详解
本文深入探讨了并发编程的关键概念和技术挑战。首先介绍了进程、线程、纤程、协程、管程等概念,强调了这些概念是如何随多核时代的到来而演变的,以满足高性能计算的需求。随后,文章详细解释了死锁、活锁与锁饥饿等问题,通过生动的例子帮助理解这些现象,并提供了预防和解决这些问题的方法。最后,通过一个具体的死锁示例代码展示了如何在实践中遇到并发问题,并提供了几种常用的工具和技术来诊断和解决这些问题。本文旨在为并发编程的实践者提供一个全面的理解框架,帮助他们在开发过程中更好地处理并发问题。
|
4月前
|
Arthas 监控 Java
深入解析与解决高并发下的线程池死锁问题
在高并发的互联网应用中,遇到线程池死锁问题导致响应延迟和超时。问题源于库存服务的悲观锁策略和线程池配置不当。通过以下方式解决:1) 采用乐观锁(如Spring Data JPA的@Version注解)替换悲观锁,减少线程等待;2) 动态调整线程池参数,如核心线程数、最大线程数和拒绝策略,以适应业务负载变化;3) 实施超时和重试机制,减少资源占用。这些改进提高了系统稳定性和用户体验。
166 2
|
4月前
|
Java
在Java中,死锁是指两个或多个线程互相等待对方释放资源,从而导致所有线程都无法继续执行的情况。
【6月更文挑战第24天】在Java并发中,死锁是多线程互相等待资源导致的僵局。避免死锁的关键策略包括:防止锁嵌套,设定固定的加锁顺序,使用`tryLock`带超时,避免无限等待,减少锁的持有时间,利用高级同步工具如`java.util.concurrent`,以及实施死锁检测和恢复机制。通过这些方法,可以提升程序的并发安全性。
39 1