锁的活跃性(死锁、活锁、饥饿)

港控/mmm° 2022-09-11 10:29 276阅读 0赞

死锁

锁是个非常有用的工具,运用场景非常多,它使用起来非常简单,而且易于理解。但
同时它也会带来一些困扰,那就是可能会引起死锁,一旦产生死锁,就会造成系统功能不可
用。

定义

线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁。

死锁的产生的一些特定条件:

1、互斥条件:即一个资源只能被一个线程占用,直到被该线程释放 。

2、请求和保持条件:一个线程因请求资源而发生阻塞时,对已获得的资源保持不放。

3、不剥夺条件:一个资源被一个线程占用时,其他线程都无法对这个资源剥夺占用。

4、循环等待条件:当发生死锁时,所等待的线程会形成一个环路(类似于死循环),造成永久阻塞。

造成死锁必须达到4个条件,避免死锁,只需不满足其中一个条件即可,前三个都是作为锁要符合的条件,所以要避免死锁就要打破第四个条件

产生死锁的主要原因

系统资源不足

进程运行推进的顺序不合适

资源分配不当

image-20210906095827548

  1. package com.dongguo.sync;
  2. import jdk.internal.dynalink.beans.StaticClass;
  3. import java.util.concurrent.TimeUnit;
  4. /** * @author Dongguo * @date 2021/8/24 0024-14:25 * @description: 模拟死锁 */
  5. public class DeadLock {
  6. static Object obj1 = new Object();
  7. static Object obj2 = new Object();
  8. public static void main(String[] args) {
  9. new Thread(() -> {
  10. synchronized (obj1) {
  11. System.out.println(Thread.currentThread().getName() + "持有锁1,试图获取锁2");
  12. try {
  13. TimeUnit.SECONDS.sleep(1);
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. synchronized (obj2) {
  18. System.out.println(Thread.currentThread().getName() + "持有锁1,获取锁2成功");
  19. }
  20. }
  21. }, "ThreadA").start();
  22. new Thread(() -> {
  23. synchronized (obj2) {
  24. System.out.println(Thread.currentThread().getName() + "持有锁2,试图获取锁1");
  25. try {
  26. TimeUnit.SECONDS.sleep(1);
  27. } catch (InterruptedException e) {
  28. e.printStackTrace();
  29. }
  30. synchronized (obj1) {
  31. System.out.println(Thread.currentThread().getName() + "持有锁2,获取锁1成功");
  32. }
  33. }
  34. }, "ThreadB").start();
  35. }
  36. }

运行结果:

image-20210903212133572

这段代码会引起死锁,使线程ThreadA和线程ThreadAB互相等待对方释放锁。

这段代码只是演示死锁的场景,在现实中你可能不会写出这样的代码。但是,在一些更为
复杂的场景中,你可能会遇到这样的问题,比如ThreadA拿到锁之后,因为一些异常情况没有释放锁
(死循环)。又或者是ThreadA拿到一个数据库锁,释放锁的时候抛出了异常,没释放掉 。那么ThreadAB

获取锁时会等待ThreadA去释放锁,这样就造成了死锁。

死锁验证

一旦出现死锁,业务是可感知的,因为不能继续提供服务了,那么只能通过dump线程查看
到底是哪个线程出现了问题

使用java命令行jstack方式

  1. C:\Users\Administrator>jps
  2. 4288 RemoteMavenServer36
  3. 10968 Jps
  4. 17352 Launcher
  5. 13212 DeadLock
  6. 13436
  7. C:\Users\Administrator>jstack 13212
  8. 2021-08-24 14:32:51
  9. Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.301-b09 mixed mode):
  10. "DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x000002b418b40000 nid=0x3fa0 waiting on condition [0x0000000000000000]
  11. java.lang.Thread.State: RUNNABLE
  12. "ThreadB" #12 prio=5 os_prio=0 tid=0x000002b42dc62800 nid=0x2ea4 waiting for monitor entry [0x000000c4c43ff000]
  13. java.lang.Thread.State: BLOCKED (on object monitor)
  14. at com.dongguo.sync.DeadLock.lambda$main$1(DeadLock.java:39)
  15. - waiting to lock <0x00000000eb4b5288> (a java.lang.Object)
  16. - locked <0x00000000eb4b5298> (a java.lang.Object)
  17. at com.dongguo.sync.DeadLock$$Lambda$2/1480010240.run(Unknown Source)
  18. at java.lang.Thread.run(Thread.java:748)
  19. "ThreadA" #11 prio=5 os_prio=0 tid=0x000002b42dc1a800 nid=0x2a00 waiting for monitor entry [0x000000c4c42ff000]
  20. java.lang.Thread.State: BLOCKED (on object monitor)
  21. at com.dongguo.sync.DeadLock.lambda$main$0(DeadLock.java:26)
  22. - waiting to lock <0x00000000eb4b5298> (a java.lang.Object)
  23. - locked <0x00000000eb4b5288> (a java.lang.Object)
  24. at com.dongguo.sync.DeadLock$$Lambda$1/2074407503.run(Unknown Source)
  25. at java.lang.Thread.run(Thread.java:748)
  26. "Service Thread" #10 daemon prio=9 os_prio=0 tid=0x000002b42d921800 nid=0x43c0 runnable [0x0000000000000000]
  27. java.lang.Thread.State: RUNNABLE
  28. "C1 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x000002b42ce5f800 nid=0xc0 waiting on condition [0x0000000000000000]
  29. java.lang.Thread.State: RUNNABLE
  30. "C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x000002b42ce5e000 nid=0xc8c waiting on condition [0x0000000000000000]
  31. java.lang.Thread.State: RUNNABLE
  32. "C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x000002b42ce5c800 nid=0x2830 waiting on condition [0x0000000000000000]
  33. java.lang.Thread.State: RUNNABLE
  34. "Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x000002b42ce59000 nid=0x41e4 runnable [0x000000c4c3cfe000]
  35. java.lang.Thread.State: RUNNABLE
  36. at java.net.SocketInputStream.socketRead0(Native Method)
  37. at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
  38. at java.net.SocketInputStream.read(SocketInputStream.java:171)
  39. at java.net.SocketInputStream.read(SocketInputStream.java:141)
  40. at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
  41. at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
  42. at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
  43. - locked <0x00000000eb492430> (a java.io.InputStreamReader)
  44. at java.io.InputStreamReader.read(InputStreamReader.java:184)
  45. at java.io.BufferedReader.fill(BufferedReader.java:161)
  46. at java.io.BufferedReader.readLine(BufferedReader.java:324)
  47. - locked <0x00000000eb492430> (a java.io.InputStreamReader)
  48. at java.io.BufferedReader.readLine(BufferedReader.java:389)
  49. at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:48)
  50. "Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000002b42cd7e800 nid=0x3e44 waiting on condition [0x0000000000000000]
  51. java.lang.Thread.State: RUNNABLE
  52. "Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x000002b42cdd5000 nid=0xde0 runnable [0x0000000000000000]
  53. java.lang.Thread.State: RUNNABLE
  54. "Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000002b42cd52800 nid=0x3004 in Object.wait() [0x000000c4c39fe000]
  55. java.lang.Thread.State: WAITING (on object monitor)
  56. at java.lang.Object.wait(Native Method)
  57. - waiting on <0x00000000eb308ee0> (a java.lang.ref.ReferenceQueue$Lock)
  58. at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
  59. - locked <0x00000000eb308ee0> (a java.lang.ref.ReferenceQueue$Lock)
  60. at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
  61. at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)
  62. "Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x000002b42cd4b800 nid=0x884 in Object.wait() [0x000000c4c38ff000]
  63. java.lang.Thread.State: WAITING (on object monitor)
  64. at java.lang.Object.wait(Native Method)
  65. - waiting on <0x00000000eb306c00> (a java.lang.ref.Reference$Lock)
  66. at java.lang.Object.wait(Object.java:502)
  67. at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
  68. - locked <0x00000000eb306c00> (a java.lang.ref.Reference$Lock)
  69. at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
  70. "VM Thread" os_prio=2 tid=0x000002b42cd21800 nid=0x324 runnable
  71. "GC task thread#0 (ParallelGC)" os_prio=0 tid=0x000002b418b55000 nid=0xd9c runnable
  72. "GC task thread#1 (ParallelGC)" os_prio=0 tid=0x000002b418b56800 nid=0x4034 runnable
  73. "GC task thread#2 (ParallelGC)" os_prio=0 tid=0x000002b418b58000 nid=0x431c runnable
  74. "GC task thread#3 (ParallelGC)" os_prio=0 tid=0x000002b418b59000 nid=0x3b54 runnable
  75. "VM Periodic Task Thread" os_prio=2 tid=0x000002b42d925000 nid=0x4108 waiting on condition
  76. JNI global references: 317
  77. Found one Java-level deadlock:
  78. =============================
  79. "ThreadB":
  80. waiting to lock monitor 0x000002b42cd4fa18 (object 0x00000000eb4b5288, a java.lang.Object),
  81. which is held by "ThreadA"
  82. "ThreadA":
  83. waiting to lock monitor 0x000002b42cd522a8 (object 0x00000000eb4b5298, a java.lang.Object),
  84. which is held by "ThreadB"
  85. Java stack information for the threads listed above:
  86. ===================================================
  87. "ThreadB":
  88. at com.dongguo.sync.DeadLock.lambda$main$1(DeadLock.java:39)
  89. - waiting to lock <0x00000000eb4b5288> (a java.lang.Object)
  90. - locked <0x00000000eb4b5298> (a java.lang.Object)
  91. at com.dongguo.sync.DeadLock$$Lambda$2/1480010240.run(Unknown Source)
  92. at java.lang.Thread.run(Thread.java:748)
  93. "ThreadA":
  94. at com.dongguo.sync.DeadLock.lambda$main$0(DeadLock.java:26)
  95. - waiting to lock <0x00000000eb4b5298> (a java.lang.Object)
  96. - locked <0x00000000eb4b5288> (a java.lang.Object)
  97. at com.dongguo.sync.DeadLock$$Lambda$1/2074407503.run(Unknown Source)
  98. at java.lang.Thread.run(Thread.java:748)
  99. Found 1 deadlock. C:\Users\Administrator>

着重看最后Java stack information for the threads listed above:

ThreadB waiting to lock <0x00000000eb4b5288> locked <0x00000000eb4b5298>

ThreadA waiting to lock <0x00000000eb4b5298> locked <0x00000000eb4b5288>

Found 1 deadlock.

ThreadB 等待锁5288 ,持有锁5298

ThreadA 等待锁5298 ,持有锁5288

Found 1 deadlock.

如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到
CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查

当然也可以使用可视化工具,比如jvisualvm

image-20210903212424893

image-20210903212452430

显示的内容是一样的

Jconsole

image-20210906104531406

image-20210906104557461

如何避免死锁:

1、加锁顺序: 注意加锁顺序,保证每个线程按照同样的顺序进行加锁

当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。当然这种方式需要你事先知道所有可能会用到的锁,然而总有些时候是无法预知的。

2、加锁时限: 针对设置一个超时时间

加上一个超时时间,若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。但是如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。

3、死锁检测:预防机制,确保第一时间发现死锁进行解决

死锁检测即每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。

避免死锁的几个常见方法。

·避免一个线程同时获取多个锁。
·避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
·尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
·对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

活锁

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,

例如

  1. package com.dongguo.lock;
  2. import java.util.concurrent.TimeUnit;
  3. /** * @author Dongguo * @date 2021/9/12 0012-14:15 * @description: */
  4. public class LiveLockDemo {
  5. static volatile int count = 10;
  6. static final Object lock = new Object();
  7. public static void main(String[] args) {
  8. new Thread(() -> {
  9. // 期望减到 0 退出循环
  10. while (count > 0) {
  11. try {
  12. TimeUnit.MILLISECONDS.sleep(200);
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. count--;
  17. System.out.println(Thread.currentThread().getName()+" count="+count);
  18. }
  19. }, "t1").start();
  20. new Thread(() -> {
  21. // 期望超过 20 退出循环
  22. while (count < 20) {
  23. try {
  24. TimeUnit.MILLISECONDS.sleep(200);
  25. } catch (InterruptedException e) {
  26. e.printStackTrace();
  27. }
  28. count++;
  29. System.out.println(Thread.currentThread().getName()+" count="+count);
  30. }
  31. }, "t2").start();
  32. }
  33. }

image-20210912141814876

count初始为10

t1线程执行count–操作,期望count值为0时继续往下执行

t2线程 执行count++操作,期望count值为20时继续往下执行

t1不断执行count–操作,t2不断执行coun++操作,导致两个线程一直无法满足条件。

饥饿

多个线程访问同一个同步资源,有些线程总是没有机会得到互斥锁,这种就叫做饥饿。

很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束

出现饥饿的三种情况

a,高优先级的线程吞噬了低优先级的线程的CPU时间片
理论上来说,线程优先级高的线程会比线程优先级低的线程获得更多的执行机会,
经过测试,优先级高的出现频率会比优先级低的高很多
不同的操作系统对线程的优先级支持是不同的,规定是在1-10之间,java通过3个常量来屏蔽这种操作系统的底层差异化。
b,线程被永久阻塞在等待进入同步代码块的状态
c,等待的线程永远不被唤醒

举例

比如synchronized是非公平锁、ReentrantLock默认是非公平锁,就可能导致一个或多个线程将资源全部抢占,导致某些线程无法获得运行的机会。

ReentrantLock非公平锁实现3个售票员卖出100张票的案例

  1. package com.dongguo.concurrent.synchronize;
  2. import java.util.concurrent.locks.Lock;
  3. import java.util.concurrent.locks.ReentrantLock;
  4. /** * @author Dongguo * @date 2021/9/3 0003-10:14 * @description: 实现3个售票员卖出100张票的案例 * 使用Lock */
  5. public class SaleTicket {
  6. public static void main(String[] args) {
  7. Ticket ticket = new Ticket();
  8. new Thread(() -> {
  9. //循环100次保证能够卖光票
  10. for (int i = 0; i < 100; i++) {
  11. ticket.saleTicket();
  12. }
  13. }, "T1").start();
  14. new Thread(() -> {
  15. for (int i = 0; i < 100; i++) {
  16. ticket.saleTicket();
  17. }
  18. }, "T2").start();
  19. new Thread(() -> {
  20. for (int i = 0; i < 100; i++) {
  21. ticket.saleTicket();
  22. }
  23. }, "T3").start();
  24. }
  25. }
  26. /** * @author Dongguo * @description: 资源类 */
  27. class Ticket {
  28. private int count = 100;
  29. //创建可重入锁ReentrantLock
  30. private Lock lock = new ReentrantLock();//默认为false
  31. public void saleTicket() {
  32. //上锁
  33. lock.lock();
  34. try {
  35. if (count > 0) {
  36. count--;
  37. System.out.println(Thread.currentThread().getName() + "卖票成功,还剩" + count + "张票!");
  38. }
  39. } catch (Exception e) {
  40. e.printStackTrace();
  41. } finally {
  42. //解锁
  43. lock.unlock();
  44. }
  45. }
  46. }

运行结果:

  1. T1卖票成功,还剩99张票!
  2. T1卖票成功,还剩98张票!
  3. T1卖票成功,还剩97张票!
  4. T1卖票成功,还剩96张票!
  5. T1卖票成功,还剩95张票!
  6. T1卖票成功,还剩94张票!
  7. T1卖票成功,还剩93张票!
  8. T1卖票成功,还剩92张票!
  9. T1卖票成功,还剩91张票!
  10. T1卖票成功,还剩90张票!
  11. T1卖票成功,还剩89张票!
  12. T1卖票成功,还剩88张票!
  13. T1卖票成功,还剩87张票!
  14. T1卖票成功,还剩86张票!
  15. T1卖票成功,还剩85张票!
  16. T1卖票成功,还剩84张票!
  17. T1卖票成功,还剩83张票!
  18. T1卖票成功,还剩82张票!
  19. T1卖票成功,还剩81张票!
  20. T1卖票成功,还剩80张票!
  21. T1卖票成功,还剩79张票!
  22. T1卖票成功,还剩78张票!
  23. T1卖票成功,还剩77张票!
  24. T1卖票成功,还剩76张票!
  25. T1卖票成功,还剩75张票!
  26. T1卖票成功,还剩74张票!
  27. T1卖票成功,还剩73张票!
  28. T1卖票成功,还剩72张票!
  29. T1卖票成功,还剩71张票!
  30. T1卖票成功,还剩70张票!
  31. T1卖票成功,还剩69张票!
  32. T1卖票成功,还剩68张票!
  33. T1卖票成功,还剩67张票!
  34. T1卖票成功,还剩66张票!
  35. T1卖票成功,还剩65张票!
  36. T1卖票成功,还剩64张票!
  37. T1卖票成功,还剩63张票!
  38. T1卖票成功,还剩62张票!
  39. T1卖票成功,还剩61张票!
  40. T1卖票成功,还剩60张票!
  41. T1卖票成功,还剩59张票!
  42. T1卖票成功,还剩58张票!
  43. T1卖票成功,还剩57张票!
  44. T1卖票成功,还剩56张票!
  45. T1卖票成功,还剩55张票!
  46. T1卖票成功,还剩54张票!
  47. T1卖票成功,还剩53张票!
  48. T1卖票成功,还剩52张票!
  49. T1卖票成功,还剩51张票!
  50. T1卖票成功,还剩50张票!
  51. T1卖票成功,还剩49张票!
  52. T1卖票成功,还剩48张票!
  53. T1卖票成功,还剩47张票!
  54. T1卖票成功,还剩46张票!
  55. T1卖票成功,还剩45张票!
  56. T1卖票成功,还剩44张票!
  57. T1卖票成功,还剩43张票!
  58. T1卖票成功,还剩42张票!
  59. T1卖票成功,还剩41张票!
  60. T1卖票成功,还剩40张票!
  61. T1卖票成功,还剩39张票!
  62. T1卖票成功,还剩38张票!
  63. T1卖票成功,还剩37张票!
  64. T1卖票成功,还剩36张票!
  65. T1卖票成功,还剩35张票!
  66. T1卖票成功,还剩34张票!
  67. T1卖票成功,还剩33张票!
  68. T1卖票成功,还剩32张票!
  69. T1卖票成功,还剩31张票!
  70. T1卖票成功,还剩30张票!
  71. T1卖票成功,还剩29张票!
  72. T1卖票成功,还剩28张票!
  73. T2卖票成功,还剩27张票!
  74. T2卖票成功,还剩26张票!
  75. T2卖票成功,还剩25张票!
  76. T2卖票成功,还剩24张票!
  77. T2卖票成功,还剩23张票!
  78. T2卖票成功,还剩22张票!
  79. T2卖票成功,还剩21张票!
  80. T2卖票成功,还剩20张票!
  81. T2卖票成功,还剩19张票!
  82. T2卖票成功,还剩18张票!
  83. T2卖票成功,还剩17张票!
  84. T2卖票成功,还剩16张票!
  85. T2卖票成功,还剩15张票!
  86. T2卖票成功,还剩14张票!
  87. T2卖票成功,还剩13张票!
  88. T2卖票成功,还剩12张票!
  89. T2卖票成功,还剩11张票!
  90. T2卖票成功,还剩10张票!
  91. T2卖票成功,还剩9张票!
  92. T2卖票成功,还剩8张票!
  93. T2卖票成功,还剩7张票!
  94. T2卖票成功,还剩6张票!
  95. T2卖票成功,还剩5张票!
  96. T2卖票成功,还剩4张票!
  97. T2卖票成功,还剩3张票!
  98. T2卖票成功,还剩2张票!
  99. T2卖票成功,还剩1张票!
  100. T2卖票成功,还剩0张票!

所有的票都被线程t1、t2卖掉了,t3线程没有运行。

比如ReentrantReadWriteLock中

一旦读操作比较多的时候,想要获取写锁就变得比较困难了,
假如当前1000个线程,999个读,1个写,有可能999个读取线程长时间抢到了锁,那1个写线程就悲剧了
因为当前有可能会一直存在读锁,而无法获得写锁,根本没机会写

如何避免饥饿问题

a,设置合理的优先级
b,使用公平锁来代替synchronized这种互斥锁(可以使用邮戳锁StampedLock替代ReentrantReadWriteLock)

发表评论

表情:
评论列表 (有 0 条评论,276人围观)

还没有评论,来说两句吧...

相关阅读

    相关 &

    一、活锁(liveLock) 活锁是指线程间资源冲突激烈,引起线程不断的尝试获取资源,不断的失败。活锁有点类似于线程饥饿,虽然资源并没有被别人持有,但由于各种原因而无法得

    相关 饥饿

    死锁 死锁是多线程编程中一种常见的问题,它发生在两个或多个线程相互等待对方释放资源的情况下。这种情况会导致所有线程都无法继续执行,程序停滞不前。典型的死锁情况包括以下几个

    相关 饥饿

    推荐:[Java并发编程汇总][Java] 死锁、饥饿和活锁 原文地址 [Java并发编程系列之十二:死锁、饥饿和活锁][Java 1] 正文 死锁发生在一个线程

    相关 &

    死锁 假设 你银行账户上有1000元钱 你老婆拿银行卡去ATM机上去取这1000元钱 银行数据库系统先确认你的账户上有这1000元 然后,银行拿钱给你老婆

    相关 饥饿解释

    死锁:是指 两个或两个以上的进程(或线程)在执行过程中,因 争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁