并发编程之死锁解析(代码片段)

莫那·鲁道 莫那·鲁道     2022-11-14     127

关键词:

技术分享图片

前言

在 Java 的并发编程中,有一个问题需要特别注意,那就是死锁,如果发生了死锁,基本就是重启,而重启将会丢失运行中的数据。所以,了解死锁的形成并排查死锁到预防死锁成了一个重要的问题。

我们了解任何一个事情的步骤是:what,how,why,why not。

1. 什么是死锁?

我们还是直接写一段代码来看看:

package hello;

public class DeadLock 

  public static void main(String[] args) 

    new Thread(() -> 
      try 
         new DeadLock().resource1();
       catch (InterruptedException e) 
      
    
    ).start();

    new Thread(() -> 
      try 
         new DeadLock().resource2();
       catch (InterruptedException e) 
      
    
    ).start();
  

  void resource1() throws InterruptedException 

    synchronized ("resource1") 
      System.out.println("获取资源1");
       // 等待 1 秒让另一个线程拿到锁
      Thread.sleep(1000);
      resource2();
    


  

  void resource2() throws InterruptedException 
    synchronized ("resource2") 
      System.out.println("获取资源2");
      // 等待 1 秒让另一个线程拿到锁
      Thread.sleep(1000);
      resource1();
    
  

上面的代码中,我们启用了两个线程,分别抢占2个资源,但这两个资源又分别被不同的对象(字符串)锁住了。当第一个线程调用 resource1 方法,进入同步块,拿到锁,并等待 1 秒钟让另一个线程进入 resource2 同步块,当第二个线程进入同步块后,注意:此时, 拿着 resourec1 锁的线程企图拿到 resource2 的锁,但这个时候,拿着 resource2 的线程也想去拿 resource1 的锁。于是就出现了互相僵持的情况,谁也无法拿到对方的锁,整个系统就卡死了。

这种情况就是死锁。

像我们现在写的代码是自己故意造出来的死锁,我们能够发现,那如果是线上环境怎么办,假如我们的系统卡死了,我们怎么知道到底是哪一段代码出现了问题,有没有可能使死锁的问题。也就是如何检测死锁。

2. 如何检测死锁?

由于死锁极难通过人工的方式查出来,因此JDK 提供了命令来检测某个java进程中心线程的情况,并排查有没有死锁。上面命令呢? jps , 用来查看java 程序的进程号,当然在 Linux 中也可以通过别的方式获取, jstack 进程号命令则可以答应对应进程的栈信息,并找到死锁。

我们就刚刚的程序,在 windows 上使用该命令。

C:\Users\stateis0>jps
11060
2084 Launcher
10712 RemoteMavenServer
18040 Jps
11820 DeadLock





C:\Users\stateis0>jstack 11820
2017-12-29 18:52:38
Full thread dump Java HotSpot(TM) Client VM (25.131-b11 mixed mode):

"DestroyJavaVM" #11 prio=5 os_prio=0 tid=0x051fe800 nid=0x1e0c waiting on condition [0x00000000]
   java.lang.Thread.State: RUNNABLE

"Thread-1" #10 prio=5 os_prio=0 tid=0x18777800 nid=0x5664 waiting for monitor entry [0x18e0f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at hello.DeadLock.resource1(DeadLock.java:31)
        - waiting to lock <0x07415a50> (a java.lang.String)
        at hello.DeadLock.resource2(DeadLock.java:43)
        - locked <0x0742bd18> (a java.lang.String)
        at hello.DeadLock.lambda$main$1(DeadLock.java:20)
        at hello.DeadLock$$Lambda$2/4983748.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

"Thread-0" #9 prio=5 os_prio=0 tid=0x18776c00 nid=0x4dc4 waiting for monitor entry [0x18d7f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at hello.DeadLock.resource2(DeadLock.java:41)
        - waiting to lock <0x0742bd18> (a java.lang.String)
        at hello.DeadLock.resource1(DeadLock.java:33)
        - locked <0x07415a50> (a java.lang.String)
        at hello.DeadLock.lambda$main$0(DeadLock.java:11)
        at hello.DeadLock$$Lambda$1/5592464.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

"Service Thread" #8 daemon prio=9 os_prio=0 tid=0x186e4c00 nid=0x172c runnable [0x00000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x186af000 nid=0x53f8 waiting on condition [0x00000000]
   java.lang.Thread.State: RUNNABLE

"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x1861e800 nid=0x3928 runnable [0x18b3f000]
   java.lang.Thread.State: RUNNABLE
        at java.net.SocketInputStream.socketRead0(Native Method)
        at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
        at java.net.SocketInputStream.read(SocketInputStream.java:171)
        at java.net.SocketInputStream.read(SocketInputStream.java:141)
        at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
        at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
        at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
        - locked <0x07861da0> (a java.io.InputStreamReader)
        at java.io.InputStreamReader.read(InputStreamReader.java:184)
        at java.io.BufferedReader.fill(BufferedReader.java:161)
        at java.io.BufferedReader.readLine(BufferedReader.java:324)
        - locked <0x07861da0> (a java.io.InputStreamReader)
        at java.io.BufferedReader.readLine(BufferedReader.java:389)
        at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:64)

"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x179c0800 nid=0x40a0 waiting on condition [0x00000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x17985c00 nid=0x5004 runnable [0x00000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x17972400 nid=0x41a8 in Object.wait() [0x17cff000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x0ca1b830> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
        - locked <0x0ca1b830> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
        at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)

"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x17960000 nid=0x4ef0 in Object.wait() [0x17c6f000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x0ca1b9d0> (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Object.java:502)
        at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
        - locked <0x0ca1b9d0> (a java.lang.ref.Reference$Lock)
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

"VM Thread" os_prio=2 tid=0x1795a800 nid=0x3f54 runnable

"VM Periodic Task Thread" os_prio=2 tid=0x18739400 nid=0x4a14 waiting on condition

JNI global references: 229

// 找到一个死锁
Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x17978de4 (object 0x07415a50, a java.lang.String),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x1797a974 (object 0x0742bd18, a java.lang.String),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
        at hello.DeadLock.resource1(DeadLock.java:31)
         // 等待 0x07415a50 锁
        - waiting to lock <0x07415a50> (a java.lang.String)
        at hello.DeadLock.resource2(DeadLock.java:43)
        // 持有 0x0742bd18
        - locked <0x0742bd18> (a java.lang.String)
        at hello.DeadLock.lambda$main$1(DeadLock.java:20)
        at hello.DeadLock$$Lambda$2/4983748.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
"Thread-0":
        at hello.DeadLock.resource2(DeadLock.java:41)
        // 等待 0x0742bd18 锁
        - waiting to lock <0x0742bd18> (a java.lang.String)
        at hello.DeadLock.resource1(DeadLock.java:33)
        // 持有 0x07415a50
        - locked <0x07415a50> (a java.lang.String)
        at hello.DeadLock.lambda$main$0(DeadLock.java:11)
        at hello.DeadLock$$Lambda$1/5592464.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

// 发现了一个死锁
Found 1 deadlock.


C:\Users\stateis0>

Thread-1 waiting to lock <0x07415a50> locked <0x0742bd18>
Thread-0 waiting to lock <0x0742bd18> locked <0x07415a50>

我们首先使用 jps 命令找到 java 进程号,然后使用 jstack 进程号 打印进程栈的信息,其中,在最后的部分,jstack 告诉我们,他找到了一个死锁,其中又详细的信息:Thread-1 线程(这里我们没有给线程其合适的名字,如果在线上,给线程起一个合适的名字将更有利于排查)持有 String 类型的编号为 0x07415a50 的锁,等待编号为 0x07415a50 的锁 , 但这个锁由 Thread-0 持有,于此同时,Thread-0 和 Thread-1 相反。Thread-0 线程持有 0x07415a50 的锁,等待 0x07415a50 的锁。我们的注释里也写上了。

那么发生了死锁,该怎么办呢?最简单的办法就是重启,重启之后,对 jstack 中打印的堆栈信息中的代码进行修改。重新发布。当然还有一些高级策略,比如让进程回滚到死锁前的状态,然后让他们顺序进入同步块。

3. 死锁有哪些形成的原因

一般来说,要出现死锁问题需要满足以下条件:

  1. 互斥条件:一个资源每次只能被一个线程使用。

  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

  3. 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。

  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

死锁是由四个必要条件导致的,所以一般来说,只要破坏这四个必要条件中的一个条件,死锁情况就应该不会发生。

如果想要打破互斥条件,我们需要允许进程同时访问某些资源,这种方法受制于实际场景,不太容易实现条件;

打破不可抢占条件,这样需要允许进程强行从占有者那里夺取某些资源,或者简单一点理解,占有资源的进程不能再申请占有其他资源,必须释放手上的资源之后才能发起申请,这个其实也很难找到适用场景;

进程在运行前申请得到所有的资源,否则该进程不能进入准备执行状态。这个方法看似有点用处,但是它的缺点是可能导致资源利用率和进程并发性降低;

避免出现资源申请环路,即对资源事先分类编号,按号分配。这种方式可以有效提高资源的利用率和系统吞吐量,但是增加了系统开销,增大了进程对资源的占用时间。

4 总结

并发编程中的坑很多,尤其死锁,造成的问题基本只能靠重启来解决,如果遇到了数据保存在内存中但没有持久化的话,那么重启将出现很大的问题。因此我们在用锁的时候,一定要小心。避免出现死锁,如果出现了死锁,则可以使用 jstack 命令查看线程是否有死锁。用以排查问题。

总之并发的坑很多,楼主以后将会多多分析。

good luck !!!!

java并发编程实战读书笔记之死锁(代码片段)

...二、动态的锁顺序死锁。前言本篇学习笔记源自于《Java并发编程实战》第10章。提示:以下是本篇文章正文内容,下面案例可供参考一、锁顺序死锁看下面代码,很容易造成死锁,leftRight和rightLeft方法分别获取了l... 查看详情

并发编程之semaphore源码解析(代码片段)

...用来做什么  semaphore是计数信号量,可用于多线程并发执行时,限制获取资源的线程数量。常用场景为:限流。 二  Semaphore用法1publicclassSemaphoreTest23publicstaticvoidmain(String[]args)4//声明5个窗口sta 查看详情

goroutine并发调度模型深度解析之手撸一个协程池(代码片段)

golanggoroutine协程池GroutinePool高并发并发(并行),一直以来都是一个编程语言里的核心主题之一,也是被开发者关注最多的话题;Go语言作为一个出道以来就自带『高并发』光环的富二代编程语言,它的并发(并行)编程肯定是... 查看详情

python并发编程之死锁

前言在并发编程中,死锁指的是一种特定的情况,即无法取得进展,程序被锁定在其当前状态。在大多数情况下,这种现象是由于不同的锁对象(用于线程同步)之间缺乏协调,或者处理不当造成的。在这一节中,我们将讨论一... 查看详情

java并发编程-一个简单的死锁示例和死锁的检查(代码片段)

  Java线程死锁是一个经典的多线程问题。因为不同的线程都在等待根本不可能被释放的锁,从而导致所有的任务都无法继续完成。1.死锁程序示例创建类DeadLockThread:publicclassDeadLockThreadimplementsRunnableprivateObjectlock1=newObject();priva... 查看详情

python并发编程多线程死锁现象与递归锁(代码片段)

  一死锁现象所谓死锁:是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相... 查看详情

测开之并发编程篇・《并发并行线程队列进程和协程》(代码片段)

并发编程并发和并行多任务概念并发和并行同步和异步多线程threading模块介绍自定义线程类线程任务函数的传递多线程资源共享和资源竞争问题GIL全局解释锁互斥锁通过锁解决资源竞争问题死锁队列队列的方法FIFO先入先出队列LI... 查看详情

10并发编程-(线程)-gil全局解释器锁&死锁与递归锁(代码片段)

一、GIL全局解释器锁1、引子在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就... 查看详情

并发编程--锁--如何使用命令行和代码定位死锁

用命令行的方式找到死锁本地环境下,如果程序发生死锁后,首先cmd进入$JAVA_HOME/bin/中,输入jps命令,就可以查看到当前Java程序的pid,找到死锁类的pid后执行jstack命令+空格+死锁类的pid,就可以获取线程获取锁的信息。截取一部... 查看详情

并发编程---死锁||递归锁---信号量---event事件---定时器(代码片段)

死锁互斥锁:Lock(),互斥锁只能acquire一次递归锁: RLock(),可以连续acquire多次,每acquire一次计数器+1,只有计数为0时,才能被抢到acquire#死锁fromthreadingimportThread,LockimporttimemutexA=Lock()mutexB=Lock()classMyThread(Thread):defrun(self):s 查看详情

java并发编程系列——死锁的解决(代码片段)

接下来我会以一个经典的转帐问题,跟大家一起聊聊死锁,以及如何解决这个问题。假如客户A需要给客户B转账,客户A的账户减少100元,客户B的账户增加100元。我们转化为代码描述:有一个账户类Account,... 查看详情

java并发编程系列——死锁的解决(代码片段)

接下来我会以一个经典的转帐问题,跟大家一起聊聊死锁,以及如何解决这个问题。假如客户A需要给客户B转账,客户A的账户减少100元,客户B的账户增加100元。我们转化为代码描述:有一个账户类Account,... 查看详情

day825.死锁问题-java并发编程实战(代码片段)

...解决银行业务里面的转账问题,虽然这个方案不存在并发问题,但是所有账户的转账操作都是串行的,例如账户A转账户B、账户C转账户D这两个转账操作现实世界里是可以并行的,但是在这个方案里却被串行化了&#x... 查看详情

day825.死锁问题-java并发编程实战(代码片段)

...解决银行业务里面的转账问题,虽然这个方案不存在并发问题,但是所有账户的转账操作都是串行的,例如账户A转账户B、账户C转账户D这两个转账操作现实世界里是可以并行的,但是在这个方案里却被串行化了&#x... 查看详情

java并发编程实战之互斥锁(代码片段)

文章目录Java并发编程实战之互斥锁如何解决原子性问题?锁模型Javasynchronized关键字Javasynchronized关键字只能解决原子性问题?如何正确使用Javasynchronized关键字?锁和受保护资源的合理关联关系死锁预防死锁破坏占有... 查看详情

java并发编程实战之互斥锁(代码片段)

文章目录Java并发编程实战之互斥锁如何解决原子性问题?锁模型Javasynchronized关键字Javasynchronized关键字只能解决原子性问题?如何正确使用Javasynchronized关键字?锁和受保护资源的合理关联关系死锁预防死锁破坏占有... 查看详情

[并发编程]产生死锁的——四大必要条件和解决方案(代码片段)

[什么是死锁?]              什么是死锁?一般对“死”字使用,在我们日常生活中都是极其避讳的,而这字偏偏又搭上一个“锁”字,就好像你永远也不可能解开你暗恋女神心中的那把锁一样...    ... 查看详情

juc并发编程活跃性--死锁&定位死锁&哲学家就餐问题(代码片段)

1.死锁有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁示例:t1线程获得A对象锁,接下来想获取B对象的锁t2线程获得B对象锁,接下来想获取A对象的锁packagetian;importlombok.extern.slf4j.Slf4j;@Slf4j(topi... 查看详情