线程活跃性问题

死锁

死锁是指两个或两个以上的线程持有不同系统资源的锁,线程彼此都等待获取对方的锁来完成自己的任务,但是没有让出自己持有的锁,线程就会无休止等待下去。下面一段代码就是一个死锁,t1先获得了A的锁,t2先获得了B的锁,随后,t1想要获得B的锁,t2想要获得A的锁,synchronized 修饰的代码块由于获得不到锁,会导致程序一直卡在互相等待双方等待锁的释放,从而导致程序无法正常运行下去。

  1. @Slf4j(topic = "c.DealLock")
  2. public class DealLock {
  3. static Object A = new Object();
  4. static Object B = new Object();
  5. public static void main(String[] args) {
  6. Thread t1 = new Thread(() -> {
  7. synchronized (A) {
  8. log.debug("lock A");
  9. try {
  10. Thread.sleep(1000);
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }
  14. synchronized (B) {
  15. log.debug("lock B");
  16. log.debug("操作...");
  17. }
  18. }
  19. }, "t1");
  20. Thread t2 = new Thread(() -> {
  21. synchronized (B) {
  22. log.debug("lock B");
  23. try {
  24. Thread.sleep(500);
  25. } catch (InterruptedException e) {
  26. e.printStackTrace();
  27. }
  28. synchronized (A) {
  29. log.debug("lock A");
  30. log.debug("操作...");
  31. }
  32. }
  33. }, "t2");
  34. t1.start();
  35. t2.start();
  36. }
  37. }

运行结果:
image.png
可以看到程序一直卡死再互相等待对方线程释放自己想要的锁,下面是示意图:
image.png
我们可以使用jpd查看Java进程,如:
image.png
然后我们可以使用“jstack 进程号”来查看是否具有死锁,如:
image.png
image.png

活锁

所谓活锁,就是两个以上的线程在执行的时候,因为相互谦让资源,结果都拿不到资源,没法运行程序。或者说,活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如下面的代码:

  1. @Slf4j(topic = "c.LiveLock")
  2. public class LiveLock {
  3. static volatile int count = 10;
  4. static final Object lock = new Object();
  5. public static void main(String[] args) {
  6. new Thread(() -> {
  7. // 期望减到 0 退出循环
  8. while (count > 0) {
  9. try {
  10. Thread.sleep(200);
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }
  14. count--;
  15. log.debug("count: {}", count);
  16. }
  17. }, "t1").start();
  18. new Thread(() -> {
  19. // 期望超过 20 退出循环
  20. while (count < 20) {
  21. try {
  22. Thread.sleep(200);
  23. } catch (InterruptedException e) {
  24. e.printStackTrace();
  25. }
  26. count++;
  27. log.debug("count: {}", count);
  28. }
  29. }, "t2").start();
  30. }
  31. }

image.png
其中一个线程count++,另一个线程count—,从结果可以看到一直在很长一段时间都在13左右徘徊,导致程序因为他们的“谦让”而一直运行。当然,死锁出现的很少,即使出现了,大部分活锁都会被系统自动解开,只是需要耗费一些时间,就好比上面的例子,在13徘徊了很久,但是还是去到了16。当然,等待系统自动解开活锁是一种消极的做法,因此我们写程序尽量避免活锁,比如上面的例子,两个线程都是“Thread.sleep(200)”,我们只需让两个线程sleep不同的时间就不会产生活锁了。

参考:https://segmentfault.com/a/1190000039249228

饥饿

如果一个线程的cpu执行时间都被其他线程抢占了,导致得不到cpu执行,这种情况就叫做“饥饿”,这个线程就会出现饥饿致死的现象,因为永远无法得到cpu的执行。解决饥饿现象的方法就是实现公平,保证所有线程都公平的获得执行的机会。
image.png

参考:https://cloud.tencent.com/developer/article/1193092

ReentrantLock介绍

简介

ReentrantLock重入锁,是实现Lock接口的一个类,是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。相比于synchronized关键字,ReentrantLock 具有 synchronized 所拥有的全部功能。此外,还有更多比 synchronized 关键字强大的功能,更加灵活,更加适合复杂的并发场景。其优势主要体现在以下几个方面:

  • 可通过 Condition 类实现多路通知功能,即支持多个条件变量(可以理解为多个 WaitSet)
  • 提供多个便利的方法,如判断是否有线程在排队等待锁
  • 可设置所为公平锁或非公平锁
  • 可以响应中断请求
  • 带超时的获取锁尝试

image.png

特点介绍

可重入性

可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁;但是如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。

  1. @Slf4j(topic = "c.ReentrantLockTest")
  2. public class ReentrantLockTest {
  3. /**
  4. * 创建一个 ReentrantLock 对象
  5. */
  6. static ReentrantLock lock = new ReentrantLock();
  7. public static void main(String[] args) {
  8. method1();
  9. }
  10. public static void method1() {
  11. lock.lock();
  12. try {
  13. log.debug("execute method1");
  14. method2();
  15. } finally {
  16. lock.unlock();
  17. }
  18. }
  19. public static void method2() {
  20. lock.lock();
  21. try {
  22. log.debug("execute method2");
  23. method3();
  24. } finally {
  25. lock.unlock();
  26. }
  27. }
  28. public static void method3() {
  29. lock.lock();
  30. try {
  31. log.debug("execute method3");
  32. } finally {
  33. lock.unlock();
  34. }
  35. }
  36. }

image.png
上面代码中,main线程一共获取了3次lock,可以证明 ReentrantLock 是可重入的。

中断性

synchronized 关键字的锁是不能中断的,如果线程获取不到锁,就会一直等待,不具有中断处理的能力,而 ReentrantLock 则具有一直中断机制。但是 ReentrantLock 的 lock() 方法不具有打断能力,需要调用 lockInterruptibly() 。lockInterruptibly()方法能够中断等待获取锁的线程。当两个线程同时获取某个锁时,假若此时线程A获取到了锁,而线程B只有等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。举个例子:

  1. @Slf4j(topic = "c.ReentrantLockTest")
  2. public class ReentrantLockTest {
  3. /**
  4. * 创建一个 ReentrantLock 对象
  5. */
  6. static ReentrantLock lock = new ReentrantLock();
  7. public static void main(String[] args) {
  8. Thread t1 = new Thread(() -> {
  9. log.debug("启动...");
  10. try {
  11. lock.lockInterruptibly();
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. log.debug("等锁的过程中被打断");
  15. return;
  16. }
  17. try {
  18. log.debug("获得了锁");
  19. } finally {
  20. lock.unlock();
  21. }
  22. }, "t1");
  23. //main 线程先获得lock
  24. lock.lock();
  25. log.debug("main获得了锁");
  26. t1.start();
  27. try {
  28. Thread.sleep(1000);
  29. t1.interrupt();
  30. log.debug("执行打断");
  31. } catch (InterruptedException e) {
  32. e.printStackTrace();
  33. } finally {
  34. //main释lock
  35. lock.unlock();
  36. }
  37. }
  38. }

image.png
可以看出抛出了中断异常。

锁超时

ReentrantLock 类提供了一个尝试获取锁的方法“isTry()”方法,如果获取锁成功则返回true,否则返回false。如果该方法不带参数,则是立刻返回结果,如果带参数,则是在指定时间内获取锁,如果过了指定时间还未获取,就返回false,或期间获取到了就返回 true 。测试例子:

  1. @Slf4j(topic = "c.ReentrantLockTest")
  2. public class ReentrantLockTest {
  3. /**
  4. * 创建一个 ReentrantLock 对象
  5. */
  6. static ReentrantLock lock = new ReentrantLock();
  7. public static void main(String[] args) {
  8. Thread t1 = new Thread(() -> {
  9. log.debug("启动...");
  10. //尝试获取锁,如果无法获取,立刻返回tryLock方法的结果
  11. if (!lock.tryLock()) {
  12. log.debug("获取立刻失败,返回");
  13. return;
  14. }
  15. try {
  16. log.debug("获得了锁");
  17. } finally {
  18. lock.unlock();
  19. }
  20. }, "t1");
  21. lock.lock();
  22. log.debug("获得了锁");
  23. t1.start();
  24. try {
  25. Thread.sleep(2000);
  26. } catch (InterruptedException e) {
  27. e.printStackTrace();
  28. } finally {
  29. lock.unlock();
  30. }
  31. }
  32. }

测试结果:
image.png
下面我们看一个在指定时间内尝试获取锁的,修改代码为:

  1. @Slf4j(topic = "c.ReentrantLockTest")
  2. public class ReentrantLockTest {
  3. /**
  4. * 创建一个 ReentrantLock 对象
  5. */
  6. static ReentrantLock lock = new ReentrantLock();
  7. public static void main(String[] args) {
  8. Thread t1 = new Thread(() -> {
  9. log.debug("启动...");
  10. //尝试获取锁,如果无法获取,立刻返回tryLock方法的结果
  11. try {
  12. if (!lock.tryLock(3, TimeUnit.SECONDS)) {
  13. log.debug("3s内获取失败,返回");
  14. return;
  15. }
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }
  19. try {
  20. log.debug("获得了锁");
  21. } finally {
  22. lock.unlock();
  23. }
  24. }, "t1");
  25. lock.lock();
  26. log.debug("获得了锁");
  27. t1.start();
  28. try {
  29. Thread.sleep(5000);
  30. } catch (InterruptedException e) {
  31. e.printStackTrace();
  32. } finally {
  33. lock.unlock();
  34. }
  35. }
  36. }

image.png
主线程需要持有锁5s,因此t1在3s不会获得锁,所以3s后返回false,然后只需if语句里面的代码。

公平锁

ReentrantLock 锁支持公平锁,所谓公平锁就是先进入等待队列的线程会优先被执行。但是ReentrantLock 默认的锁还是非公平锁,其实公平锁一般没有必要使用,会降低并发度,后面分析原理时会讲解。如果要设置为公平锁,只需要修改其中的一个属性即可,我们在构造锁对象是参数填写为true即可,如:“ReentrantLock lock = new ReentrantLock(true);”我们先来验证默认的锁为非公平锁,如下代码:

  1. @Slf4j(topic = "c.ReentrantLockTest")
  2. public class ReentrantLockTest {
  3. /**
  4. * 创建一个 ReentrantLock 对象
  5. */
  6. static ReentrantLock lock = new ReentrantLock();
  7. public static void main(String[] args) throws InterruptedException {
  8. lock.lock();
  9. for (int i = 0; i < 500; i++) {
  10. new Thread(() -> {
  11. lock.lock();
  12. try {
  13. System.out.println(Thread.currentThread().getName() + " running...");
  14. } finally {
  15. lock.unlock();
  16. }
  17. }, "t" + i).start();
  18. }
  19. // 1s 之后去争抢锁
  20. Thread.sleep(1000);
  21. new Thread(() -> {
  22. lock.lock();
  23. try {
  24. System.out.println(Thread.currentThread().getName() + " running...");
  25. } finally {
  26. lock.unlock();
  27. }
  28. }, "可恶的插队线程").start();
  29. lock.unlock();
  30. }
  31. }

运行结果:
image.png
可以看到“可恶的插队线程”插队成功了,因此证明了默认是非公平锁。然后我们修改代码,验证公平锁:

  1. static ReentrantLock lock = new ReentrantLock(true);

image.png
无论运行多少次,都不会存在这个插队现象的。

条件变量

在学习 synchronized 关键字的实现原理时,知道了它只有一个 WaitSet ,在 wait-notify 机制的学习中的“小南和小女分别等待烟和面包”的例子中可以验证。但是 ReentrantLock 却可以创建多个条件变量(即等待室),也就是多个 Condition 对象。
image.png
再来根据那个例子来说明:

  1. @Slf4j(topic = "c.ReentrantLockTest")
  2. public class ReentrantLockTest {
  3. static ReentrantLock lock = new ReentrantLock();
  4. static Condition waitCigaretteQueue = lock.newCondition();
  5. static Condition waitbreakfastQueue = lock.newCondition();
  6. static volatile boolean hasCigrette = false;
  7. static volatile boolean hasBreakfast = false;
  8. public static void main(String[] args) throws InterruptedException {
  9. new Thread(() -> {
  10. try {
  11. lock.lock();
  12. while (!hasCigrette) {
  13. try {
  14. waitCigaretteQueue.await();
  15. } catch (InterruptedException e) {
  16. e.printStackTrace();
  17. }
  18. }
  19. log.debug("等到了它的烟");
  20. } finally {
  21. lock.unlock();
  22. }
  23. },"小南").start();
  24. new Thread(() -> {
  25. try {
  26. lock.lock();
  27. while (!hasBreakfast) {
  28. try {
  29. waitbreakfastQueue.await();
  30. } catch (InterruptedException e) {
  31. e.printStackTrace();
  32. }
  33. }
  34. log.debug("等到了它的早餐");
  35. } finally {
  36. lock.unlock();
  37. }
  38. },"小女").start();
  39. Thread.sleep(1000);
  40. sendBreakfast();
  41. Thread.sleep(1000);
  42. sendCigarette();
  43. }
  44. private static void sendCigarette() {
  45. lock.lock();
  46. try {
  47. log.debug("送烟来了");
  48. hasCigrette = true;
  49. waitCigaretteQueue.signal();
  50. } finally {
  51. lock.unlock();
  52. }
  53. }
  54. private static void sendBreakfast() {
  55. lock.lock();
  56. try {
  57. log.debug("送早餐来了");
  58. hasBreakfast = true;
  59. waitbreakfastQueue.signal();
  60. } finally {
  61. lock.unlock();
  62. }
  63. }
  64. }

不难看出,signal 就相当于 notify,同样的 signalAll 就相当于 notifyAll ;而 await 就相当于 wait,同样,await也可以具有时间限制的等待。运行结果如下:
image.png