5.1 显式锁

Java内置锁的功能相对单一,不具备一些比较高级的锁功能,比如:

  1. 限时抢锁:在抢锁时设置超时时长,如果超时还未获得锁就放弃,不至于无限等下去。
  2. 可中断抢锁:在抢锁时,外部线程给抢锁线程发一个中断信号,就能唤起等待锁的线程,并终止抢占过程。
  3. 多个等待队列:为锁维持多个等待队列,以便提高锁的效率。比如在生产者-消费者模式实现中,生产者和消费者共用一把锁,该锁上维持两个等待队列,即一个生产者队列和一个消费者队列。

    5.1.1 显式锁Lock接口

    Lock接口的主要抽象方法
    image.png

    5.1.2 可重入锁ReentrantLock

  4. 可重入的含义: 表示该锁能够支持一个线程对资源的重复加锁,也就是说,一个线程可以多次进入同一个锁所同步的临界区代码块。比如,同一线程在外层函数获得锁后,在内层函数能再次获取该锁,甚至多次抢占到同一把锁。

下面是一段对可重入锁进行两次抢占和释放的伪代码,具体如下:

  1. lock.lock(); // 第一次获取锁
  2. lock.lock(); // 第二次获取锁,重新进入
  3. try {
  4. // 临界区代码块
  5. } finally {
  6. lock.unlock(); // 释放锁
  7. lock.unlock(); // 第二次释放锁
  8. }
  1. 独占的含义:在同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能等待,只有拥有锁的线程释放了锁后,其他的线程才能够获取锁。

一个简单地使用ReentrantLock进行同步累加的演示案例如下:

  1. package com.crazymakercircle.demo.lock;
  2. // 省略import
  3. public class LockTest
  4. {
  5. @org.junit.Test
  6. public void testReentrantLock()
  7. {
  8. // 每个线程的执行轮数
  9. final int TURNS = 1000;
  10. // 线程数
  11. final int THREADS = 10;
  12. //线程池,用于多线程模拟测试
  13. ExecutorService pool = Executors.newFixedThreadPool(THREADS);
  14. //创建一个可重入、独占的锁对象
  15. Lock lock = new ReentrantLock();
  16. // 倒数闩
  17. CountDownLatch countDownLatch = new CountDownLatch(THREADS);
  18. long start = System.currentTimeMillis();
  19. //10个线程并发执行
  20. for (int i = 0; i < THREADS; i++)
  21. {
  22. pool.submit(() ->
  23. {
  24. try
  25. {
  26. //累加 1000 次
  27. for (int j = 0; j < TURNS; j++)
  28. {
  29. //传入锁,执行一次累加
  30. IncrementData.lockAndFastIncrease(lock);
  31. }
  32. Print.tco("本线程累加完成");
  33. } catch (Exception e)
  34. {
  35. e.printStackTrace();
  36. }
  37. //线程执行完成,倒数闩减少一次
  38. countDownLatch.countDown();
  39. });
  40. }

出于“分离变与不变”的设计原则,这里将临界区使用锁的代码进行了抽取和封装,形成一个可以复用的独立类——IncrementData累加类,具体代码如下:

  1. package com.crazymakercircle.demo.lock;
  2. // 省略import
  3. //封装锁的使用代码
  4. public class IncrementData
  5. {
  6. public static int sum = 0;
  7. public static void lockAndFastIncrease(Lock lock)
  8. {
  9. lock.lock(); //step1:抢占锁
  10. try
  11. {
  12. //step2:执行临界区代码
  13. sum++;
  14. } finally
  15. {
  16. lock.unlock(); //step3:释放锁
  17. }
  18. }
  19. // 省略其他代码
  20. }

5.1.3 使用显式锁的模板代码

  1. 使用lock()方法抢锁的模板代码 ```java //创建锁对象,SomeLock为Lock的某个实现类,如ReentrantLock Lock lock = new SomeLock(); lock.lock(); //step1:抢占锁 try {
    1. //step2:抢锁成功,执行临界区代码
    } finally {
    1. lock.unlock(); //step3:释放锁
    }
  1. 2. 调用tryLock()方法非阻塞抢锁的模板代码
  2. 调用tryLock()方法非阻塞抢占锁,大致的模板代码如下:
  3. ```java
  4. //创建锁对象,SomeLock为Lock的某个实现类,如ReentrantLock
  5. Lock lock = new SomeLock();
  6. if (lock.tryLock()) { //step1:尝试抢占锁
  7. try {
  8. //step2:抢锁成功,执行临界区代码
  9. } finally {
  10. lock.unlock(); //step3:释放锁
  11. }
  12. }
  13. else
  14. {
  15. //step4:抢锁失败,执行后备动作
  16. }
  1. 调用tryLock(long time,TimeUnit unit)方法抢锁的模板代码 ```java //创建锁对象,SomeLock为Lock的某个实现类,如ReentrantLock Lock lock = new SomeLock(); //抢锁时阻塞一段时间,如1秒 if (lock.tryLock(1, TimeUnit.SECONDS)) { //step1:限时阻塞抢占

    1. try {
    2. //step2:抢锁成功,执行临界区代码
    3. } finally {
    4. lock.unlock(); //step3:释放锁
    5. }

    } else {

    1. //限时抢锁失败,执行后备操作

    }

  1. <a name="JhvDl"></a>
  2. ## 5.1.4 基于显式锁进行“等待-通知”方式的线程间通信
  3. 与Object对象的wait、notify两类方法相类似,基于Lock显式锁,JUC也为大家提供了一个用于线程间进行“等待-通知”方式通信的接口——java.util.concurrent.locks.Condition
  4. 1. Condition接口的主要方法
  5. ```java
  6. public interface Condition
  7. {
  8. //方法1:等待。此方法在功能上与 Object.wait()语义等效
  9. //使当前线程加入 await() 等待队列中,并释放当前锁
  10. //当其他线程调用signal()时,等待队列中的某个线程会被唤醒,重新去抢锁
  11. void await() throws InterruptedException;
  12. //方法2:通知。此方法在功能上与Object.notify()语义等效
  13. // 唤醒一个在 await()等待队列中的线程
  14. void signal();
  15. //方法3:通知全部。唤醒 await()等待队列中所有的线程
  16. //此方法与object.notifyAll()语义上等效
  17. void signalAll();
  18. //方法4:限时等待。此方法与await()语义等效
  19. //不同点在于,在指定时间time等待超时后,如果没有被唤醒,线程将中止等待
  20. //线程等待超时返回false,其他情况返回true
  21. boolean await(long time, TimeUnit unit) throws InterruptedException;
  22. }
  1. 显式锁Condition演示案例 ```java package com.crazymakercircle.demo.lock; // 省略import public class ReentrantCommunicationTest {

    1. // 创建一个显式锁
    2. static Lock lock = new ReentrantLock();
    3. // 获取一个显式锁绑定的Condition对象
    4. static private Condition condition = lock.newCondition();
    5. // 等待线程的异步目标任务
    6. static class WaitTarget implements Runnable
    7. {
    8. public void run()
    9. {
    10. lock.lock(); // ①抢锁
    11. try
    12. {
    13. Print.tcfo("我是等待方");
    14. condition.await(); // ② 开始等待,并且释放锁
    15. Print.tco("收到通知,等待方继续执行");
    16. } catch (InterruptedException e)
    17. {
    18. e.printStackTrace();
    19. } finally
    20. {
    21. lock.unlock();//释放锁
    22. }
    23. }
    24. }
    25. //通知线程的异步目标任务
    26. static class NotifyTarget implements Runnable
    27. {
    28. public void run()
    29. {
    30. lock.lock(); //③抢锁
    31. try
    32. {
    33. Print.tcfo("我是通知方");
    34. condition.signal(); // ④发送通知
    35. Print.tco("发出通知了,但是线程还没有立马释放锁");
    36. } finally
    37. {
    38. lock.unlock(); //⑤释放锁之后,等待线程才能获得锁
    39. }
    40. }
    41. }
    42. public static void main(String[] args) throws InterruptedException
    43. {
    44. //创建等待线程
    45. Thread waitThread = new Thread(new WaitTarget(), "WaitThread");
    46. //启动等待线程
    47. waitThread.start();
    48. sleepSeconds(1); //稍等一下
    49. //创建通知线程
    50. Thread notifyThread = new Thread(new NotifyTarget(), "NotifyThread");
    51. //启动通知线程
    52. notifyThread.start();
    53. }

    }

  1. <a name="pILQn"></a>
  2. ## 5.1.5 LockSupport
  3. LockSupport是JUC提供的一个线程阻塞与唤醒的工具类,该工具类可以让线程在任意位置阻塞和唤醒,其所有的方法都是静态方法。
  4. 1. LockSupport的常用方法
  5. ```java
  6. // 无限期阻塞当前线程
  7. public static void park();
  8. // 唤醒某个被阻塞的线程
  9. public static void unpark(Thread thread);
  10. // 阻塞当前线程,有超时时间的限制
  11. public static void parkNanos(long nanos);
  12. // 阻塞当前线程,直到某个时间
  13. public static void parkUntil(long deadline);
  14. // 无限期阻塞当前线程,带blocker对象,用于给诊断工具确定线程受阻塞的原因
  15. public static void park(Object blocker);
  16. // 限时阻塞当前线程,带blocker对象
  17. public static void parkNanos(Object blocker, long nanos);
  18. // 获取被阻塞线程的blocker对象,用于分析阻塞的原因
  19. public static Object getBlocker(Thread t);
  1. LockSupport的演示实例 ```java package com.crazymakercircle.demo.lock; // 省略import public class LockSupportDemo {

    1. public static class ChangeObjectThread extends Thread
    2. {
    3. public ChangeObjectThread(String name)
    4. {
    5. super(name);
    6. }
    7. @Override
    8. public void run()
    9. {
    10. Print.tco("即将进入无限时阻塞");
    11. //阻塞当前线程
    12. LockSupport.park();
    13. if (Thread.currentThread().isInterrupted())
    14. {
    15. Print.tco("被中断了,但仍然会继续执行");
    16. } else
    17. {
    18. Print.tco("被重新唤醒了");
    19. }
    20. }
    21. }
    22. //LockSupport 测试用例
    23. @org.junit.Test
    24. public void testLockSupport()
    25. {
    26. ChangeObjectThread t1 = new ChangeObjectThread("线程一");
    27. ChangeObjectThread t2 = new ChangeObjectThread("线程二");
    28. //启动线程一
    29. t1.start();
    30. sleepSeconds(1);
    31. //启动线程二
    32. t2.start();
    33. sleepSeconds(1);
    34. //中断线程一
    35. t1.interrupt();
    36. //唤醒线程二
    37. LockSupport.unpark(t2);
    38. }

    }

  1. 3. LockSupport.park()和Thread.sleep()的区别
  2. 3. LockSupport.park()与Object.wait()的区别
  3. 下面的演示代码演示在LockSupport.park()执行之前,通过执行LockSupport.unPark()唤醒一个线程,具体如下:
  4. ```java
  5. package com.crazymakercircle.demo.lock;
  6. // 省略import
  7. public class LockSupportDemo
  8. {
  9. @org.junit.Test
  10. public void testLockSupport2()
  11. {
  12. Thread t1 = new Thread(() ->
  13. {
  14. try
  15. {
  16. Thread.sleep(1000); //使sleep阻塞当前线程,时长为1秒
  17. } catch (InterruptedException e)
  18. {
  19. e.printStackTrace();
  20. }
  21. Print.tco("即将进入无限时阻塞");
  22. //使用LockSupport.park()阻塞当前线程
  23. LockSupport.park();
  24. Print.tco("被重新唤醒了");
  25. }, "演示线程"); //通过匿名对象创建一个线程
  26. t1.start();
  27. //唤醒一次没有使用 LockSupport.park()阻塞的线程
  28. LockSupport.unpark(t1);
  29. //再唤醒一次没有调用 LockSupport.park()阻塞的线程
  30. LockSupport.unpark(t1);
  31. sleepSeconds(2);
  32. //中断线程一
  33. //第三唤醒调用 LockSupport.park()阻塞的线程
  34. LockSupport.unpark(t1);
  35. }
  36. // 省略其他
  37. }

5.1.6 显式锁的分类

显式锁有很多种,从不同的角度来看,显式锁大概有以下几种分类:可重入锁和不可重入锁、悲观锁和乐观锁、公平锁和非公平锁、共享锁和独占锁、可中断锁和不可中断锁。

  1. 可重入锁和不可重入锁
  2. 悲观锁和乐观锁
  3. 公平锁和非公平锁
  4. 可中断锁和不可中断锁
  5. 共享锁和独占锁

    5.2 悲观锁和乐观锁

    独占锁其实就是一种悲观锁,Java的synchronized是悲观锁。悲观锁可以确保无论哪个线程持有锁,都能独占式访问临界区。虽然悲观锁的逻辑非常简单,但是存在不少问题。

    5.2.1 悲观锁存在的问题

  6. 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题

  7. 一个线程持有锁后,会导致其他所有抢占此锁的线程挂起。
  8. 如果一个优先级高的线程等待一个优先级低的线程释放锁,就会导致线程的优先级倒置,从而引发性能风险。

5.2.2 通过CAS实现乐观锁

5.2.3 不可重入的自旋锁

作为演示,这里先实现一个简单版本的自旋锁——不可重入的自旋锁,具体的代码如下:

  1. package com.crazymakercircle.demo.lock.custom;
  2. // 省略import
  3. public class SpinLock implements Lock
  4. {
  5. /**当前锁的拥有者
  6. * 使用Thread 作为同步状态
  7. */
  8. private AtomicReference<Thread> owner = new AtomicReference<>();
  9. /**
  10. * 抢占锁
  11. */
  12. @Override
  13. public void lock()
  14. {
  15. Thread t = Thread.currentThread();
  16. //自旋
  17. while (owner.compareAndSet(null, t))
  18. {
  19. // DO nothing
  20. Thread.yield();//让出当前剩余的CPU时间片
  21. }
  22. }
  23. /**
  24. * 释放锁
  25. */
  26. @Override
  27. public void unlock()
  28. {
  29. Thread t = Thread.currentThread();
  30. //只有拥有者才能释放锁
  31. if (t == owner.get())
  32. {
  33. // 设置拥有者为空,这里不需要 compareAndSet操作
  34. // 因为已经通过owner做过线程检查
  35. owner.set(null);
  36. }
  37. }
  38. // 省略其他代码
  39. }

5.2.4 可重入的自旋锁

为了实现可重入锁,这里引入一个计数器,用来记录一个线程获取锁的次数。一个简单的可重入的自旋锁的代码大致如下:

  1. package com.crazymakercircle.demo.lock.custom;
  2. // 省略import
  3. public class ReentrantSpinLock implements Lock
  4. {
  5. /**当前锁的拥有者
  6. * 使用拥有者 Thread 作为同步状态,而不是使用一个简单的整数作为同步状态
  7. */
  8. private AtomicReference<Thread> owner = new AtomicReference<>();
  9. /**
  10. * 记录一个线程重复获取锁的次数
  11. * 此变量为同一个线程在操作,没有必要加上volatile保障可见性和有序性
  12. */
  13. private int count = 0;
  14. /**
  15. * 抢占锁
  16. */
  17. @Override
  18. public void lock()
  19. {
  20. Thread t = Thread.currentThread();
  21. // 如果是重入,增加重入次数后返回
  22. if (t == owner.get())
  23. {
  24. ++count;
  25. return;
  26. }
  27. //自旋
  28. while (owner.compareAndSet(null, t))
  29. {
  30. // DO nothing
  31. Thread.yield();//让出当前剩余的CPU时间片
  32. }
  33. }
  34. /**
  35. * 释放锁
  36. */
  37. @Override
  38. public void unlock()
  39. {
  40. Thread t = Thread.currentThread();
  41. //只有拥有者才能释放锁
  42. if (t == owner.get())
  43. {
  44. if (count > 0)
  45. {
  46. // 如果重入的次数大于0, 减少重入次数后返回
  47. --count;
  48. } else
  49. {
  50. // 设置拥有者为空
  51. // 这里不需要 compareAndSet, 因为已经通过owner做过线程检查
  52. owner.set(null);
  53. }
  54. }
  55. }
  56. // 省略其他代码
  57. }

自旋锁的问题:
在争用激烈的场景下,如果某个线程持有锁的时间太长,就会导致其他空自旋的线程耗尽CPU资源。另外,如果大量的线程进行空自旋,还可能导致硬件层面的“总线风暴”。

5.2.5 CAS可能导致“总线风暴”

下面是sun.misc.Unsafe类的compareAndSwapInt()方法的源代码:

  1. public final class Unsafe {
  2. //Unsafe中的CAS操作
  3. public final native boolean compareAndSwapInt(
  4. Object o, //操作对象
  5. long offset, //字段偏移
  6. int expected, //预期值
  7. int x); //待更新的值
  8. // 省略不相干代码
  9. }

5.2.6 CLH自旋锁

  1. 实现CLH锁的一个学习版本 ```java package com.crazymakercircle.demo.lock.custom; // 省略import public class CLHLock implements Lock {

    1. /**
    2. * 当前节点的线程本地变量
    3. */
    4. private static ThreadLocal<Node> curNodeLocal = new ThreadLocal();
    5. /**
    6. * CLHLock队列的尾部指针,使用AtomicReference,方便进行CAS操作
    7. */
    8. private AtomicReference<Node> tail = new AtomicReference<>(null);
    9. public CLHLock()
    10. {
    11. //设置尾部节点
    12. tail.getAndSet(Node.EMPTY);
    13. }
    14. //加锁操作:将节点添加到等待队列的尾部
    15. @Override
    16. public void lock()
    17. {
    18. Node curNode = new Node(true, null);
    19. Node preNode = tail.get();
    20. //CAS自旋:将当前节点插入队列的尾部
    21. while (!tail.compareAndSet(preNode, curNode))
    22. {
    23. preNode = tail.get();
    24. }
    25. //设置前驱节点
    26. curNode.setPrevNode(preNode);
    27. // 自旋,监听前驱节点的locked变量,直到其值为false
    28. // 若前驱节点的locked状态为true,则表示前一个线程还在抢占或者占有锁
    29. while (curNode.getPrevNode().isLocked())
    30. {
    31. //让出CPU时间片,提高性能
    32. Thread.yield();
    33. }
    34. // 能执行到这里,说明当前线程获取到了锁
    35. // Print.tcfo("获取到了锁!!!");
    36. //将当前节点缓存在线程本地变量中,释放锁会用到
    37. curNodeLocal.set(curNode);
    38. }
    39. //释放锁
    40. @Override
    41. public void unlock()
    42. {
    43. Node curNode = curNodeLocal.get();
    44. curNode.setLocked(false);
    45. curNode.setPrevNode(null); //help for GC
    46. curNodeLocal.set(null); //方便下一次抢锁
    47. }
    48. //虚拟等待队列的节点
    49. @Data
    50. static class Node
    51. {
    52. public Node(boolean locked, Node prevNode)
    53. {
    54. this.locked = locked;
    55. this.prevNode = prevNode;
    56. }
    57. // true:当前线程正在抢占锁,或者已经占有锁
    58. // false:当前线程已经释放锁,下一个线程可以占有锁了
    59. volatile boolean locked;
    60. // 前一个节点,需要监听其locked字段
    61. Node prevNode;
    62. // 空节点
    63. public static final Node EMPTY = new Node(false, null);
    64. }
    65. // 省略其他代码

    }

  1. 3. CLH锁的原理分析
  2. 3. 举例说明:CLH锁的抢占过程
  3. 3. 举例说明:CLH锁的释放过程
  4. 3. CLH锁的优缺点
  5. <a name="esjQ7"></a>
  6. # 5.3 公平锁与非公平锁
  7. <a name="nhlQE"></a>
  8. ## 5.3.1 非公平锁实战
  9. 使用ReentrantLock锁作为非公平锁的实战用例,具体代码如下:
  10. ```java
  11. package com.crazymakercircle.basic.demo.lock;
  12. // 省略import
  13. public class LockTest
  14. {
  15. /**
  16. * 非公平锁测试用例
  17. */
  18. @org.junit.Test
  19. public void testNotFairLock() throws InterruptedException
  20. {
  21. //创建可重入锁,默认的非公平锁
  22. Lock lock = new ReentrantLock(false);
  23. //创建Runnable可执行实例
  24. Runnable r = () -> IncrementData.lockAndIncrease(lock);
  25. //创建4个线程
  26. Thread[] tArray = new Thread[4];
  27. for (int i = 0; i < 4; i++)
  28. {
  29. tArray[i] = new Thread(r, "线程" + i);
  30. }
  31. //启动4个线程
  32. for (int i = 0; i < 4; i++)
  33. {
  34. tArray[i].start();
  35. }
  36. Thread.sleep(Integer.MAX_VALUE);
  37. }
  38. // 省略其他代码
  39. }

5.3.2 公平锁实战

什么是公平锁呢?公平锁是指多个线程按照申请锁的顺序来获取锁,抢锁成功的次序体现为FIFO(先进先出)顺序。虽然ReentrantLock锁默认是非公平锁,但可以通过构造器指定该锁为公平锁,具体的代码如下:

  1. //可重入、公平锁对象
  2. Lock lock = new ReentrantLock(true);

下面是一个简单的公平锁实战案例。此实战案例并没有使用ReentrantLock锁,而是使用前面自定义的CLHLock锁进行演示,具体的实战代码如下:

  1. package com.crazymakercircle.basic.demo.lock;
  2. // 省略import
  3. public class LockTest
  4. {
  5. /**
  6. * 公平锁测试用例
  7. */
  8. @org.junit.Test
  9. public void testFairLock() throws InterruptedException
  10. {
  11. //创建为公平锁的类型
  12. Lock lock = new CLHLock();
  13. //创建Runnable可执行实例
  14. Runnable r = () -> IncrementData.lockAndIncrease(lock);
  15. //创建4个线程
  16. Thread[] tArray = new Thread[4];
  17. for (int i = 0; i < 4; i++)
  18. {
  19. tArray[i] = new Thread(r, "线程" + i);
  20. }
  21. //启动4个线程
  22. for (int i = 0; i < 4; i++)
  23. {
  24. tArray[i].start();
  25. }
  26. Thread.sleep(Integer.MAX_VALUE);
  27. }
  28.    // 省略其他代码
  29. }

5.4 可中断锁与不可中断锁

可中断锁是指抢占过程可以被中断的锁,JUC的显式锁(如ReentrantLock)是一个可中断锁。不可中断锁是指抢占过程不可以被中断的锁,如Java的synchronized内置锁就是一个不可中断锁。

5.4.1 锁的可中断抢占

  1. lockInterruptibly()
  2. tryLock(long timeout,TimeUnit unit) ```java package com.crazymakercircle.demo.lock; // 省略import public class IncrementData {
    1. public static int sum = 0;
    2. //演示方法:可中断抢锁
    3. public static void lockInterruptiblyAndIncrease(Lock lock)
    4. {
    5. Print.synTco(" 开始抢占锁");
    6. try
    7. {
    8. lock.lockInterruptibly();
    9. } catch (InterruptedException e)
    10. {
    11. Print.synTco("抢占被中断,抢锁失败");
    12. // e.printStackTrace();
    13. return;
    14. }
    15. try
    16. {
    17. Print.synTco("抢到了锁,同步执行1秒");
    18. sleepMilliSeconds(1000);
    19. sum++;
    20. if (Thread.currentThread().isInterrupted())
    21. {
    22. Print.synTco("同步执行被中断");
    23. }
    24. } catch (Exception e)
    25. {
    26. e.printStackTrace();
    27. } finally
    28. {
    29. lock.unlock();
    30. }
    31. }
    32. // 省略其他代码
    }
  1. 以上代码的测试用例具体如下:
  2. ```java
  3. package com.crazymakercircle.basic.demo.lock;
  4. // 省略import
  5. public class LockTest
  6. {
  7. //测试用例:抢锁过程可中断
  8. @org.junit.Test
  9. public void testInterruptLock() throws InterruptedException
  10. {
  11. //创建可重入锁,默认的非公平锁
  12. Lock lock = new ReentrantLock();
  13. //创建Runnable可执行任务实例
  14. Runnable r = () -> IncrementData.lockInterruptiblyAndIncrease(lock);
  15. Thread t1 = new Thread(r, "thread-1"); //创建第1条线程
  16. Thread t2 = new Thread(r, "thread-2"); //创建第2条线程
  17. t1.start(); //启动第1个线程
  18. t2.start(); //启动第2个线程
  19. sleepMilliSeconds(100);
  20. Print.synTco( "等待100毫秒,中断两个线程");
  21. t1.interrupt(); //启动第1个线程
  22. t2.interrupt(); //启动第2个线程
  23. Thread.sleep(Integer.MAX_VALUE);
  24. }
  25. // 省略其他代码
  26. }

5.4.2 死锁的监测与中断

JDK 8中包含的ThreadMXBean接口提供了多种监视线程的方法,其中包括两个死锁监测的方法,具体如下:

  1. findDeadlockedThreads
  2. findMonitorDeadlockedThreads

ThreadMXBean的实例可以通过JVM管理工厂ManagementFactory去获取,具体的获取代码如下:

  1. //获取ThreadMXBean的实例
  2. public static ThreadMXBean mbean = ManagementFactory.getThreadMXBean();
  1. 在这里举一个死锁监测与中断的案例。首先定义一段需要抢占两把锁才能进入的临界区代码,具体如下:
  1. package com.crazymakercircle.demo.lock;
  2. // 省略import
  3. public class TwoLockDemo
  4. {
  5. //演示代码:使用两把锁,通过可以中断的方式抢锁
  6. public static void useTowlockInterruptiblyLock(
  7. Lock lock1, Lock lock2) {
  8. String lock1Name =
  9. lock1.toString().replace("java.util.concurrent.locks.", "");
  10. String lock2Name =
  11. lock2.toString().replace("java.util.concurrent.locks.", "");
  12. Print.synTco(" 开始抢第一把锁, 为:" + lock1Name);
  13. try
  14. {
  15. lock1.lockInterruptibly();
  16. } catch (InterruptedException e)
  17. {
  18. Print.synTco(" 被中断,抢第一把锁失败, 为:" + lock1Name);
  19. //e.printStackTrace();
  20. return;
  21. }
  22. try
  23. {
  24. Print.synTco(" 抢到了第一把锁, 为:" + lock1Name);
  25. Print.synTco(" 开始抢第二把锁, 为:" + lock2Name);
  26. try
  27. {
  28. lock2.lockInterruptibly();
  29. } catch (InterruptedException e)
  30. {
  31. Print.synTco(" 被中断,抢第二把锁失败,为:" + lock2Name);
  32. //e.printStackTrace();
  33. return;
  34. }
  35. try
  36. {
  37.          Print.synTco(" 抢到了第二把锁:" + lock2Name);
  38. Print.synTco("do something ");
  39. //等待1000ms
  40. sleepMilliSeconds(1000);
  41. } catch (Exception e)
  42. {
  43. e.printStackTrace();
  44. } finally
  45. {
  46. lock2.unlock();
  47. Print.synTco(" 释放了第二把锁, 为:" + lock2Name);
  48. }
  49. } catch (Exception e)
  50. {
  51. e.printStackTrace();
  52. } finally
  53. {
  54. lock1.unlock();
  55. Print.synTco(" 释放了第一把锁, 锁为:" + lock1Name);
  56. }
  57. }

以上代码的测试用例如下:

  1. package com.crazymakercircle.basic.demo.lock;
  2. // 省略import
  3. public class LockTest
  4. {
  5. //获取ThreadMXBean
  6. public static ThreadMXBean mbean = ManagementFactory.getThreadMXBean();
  7. //测试用例:抢占两把锁,造成死锁,然后进行死锁监测和部分中断
  8. @org.junit.Test
  9. public void testDeadLock() throws InterruptedException
  10. {
  11. //创建可重入锁,默认的非公平锁
  12. Lock lock1 = new ReentrantLock();
  13. Lock lock2 = new ReentrantLock();
  14. //Runnable异步执行目标实例1: 先抢占lock1,再抢占lock2
  15. Runnable r1 = () ->
  16. TwoLockDemo.useTowlockInterruptiblyLock(lock1, lock2);
  17. //Runnable异步执行目标实例2: 先抢占lock2,再抢占 lock1
  18. Runnable r2 = () ->
  19. TwoLockDemo.useTowlockInterruptiblyLock(lock2, lock1);
  20. Thread t1 = new Thread(r1, "thread-1"); //创建第1个线程
  21. Thread t2 = new Thread(r2, "thread-2"); //创建第2个线程
  22. t1.start(); //启动第1个线程
  23. t2.start(); //启动第2个线程
  24. //等待一段时间再执行死锁监测
  25. Thread.sleep(2000);
  26. Print.tcfo("等待2秒,开始死锁监测和处理");
  27. //获取到所有死锁线程的id
  28. long[] deadlockedThreads = mbean.findDeadlockedThreads();
  29. if (deadlockedThreads.length > 0)
  30. {
  31. Print.tcfo("发生了死锁,输出死锁线程的信息");
  32. //遍历数组获取所有的死锁线程id
  33. for (long pid : deadlockedThreads)
  34. {
  35. //此方法用于获取不带有堆栈跟踪信息的线程数据
  36. //hreadInfo threadInfo = mbean.getThreadInfo(pid);
  37. //此方法用于获取带有堆栈跟踪信息的线程数据
  38. ThreadInfo threadInfo = mbean.getThreadInfo(
  39. pid, Integer.MAX_VALUE);
  40. Print.tcfo(threadInfo);
  41. }
  42. Print.tcfo("中断一个死锁线程,这里是线程:" + t1.getName());
  43. t1.interrupt(); //中断一个死锁线程
  44. }
  45. }
  46. // 省略其他代码
  47. }

5.5 共享锁与独占锁

5.5.1 独占锁

5.5.2 共享锁Semaphore

共享锁使用示例

  1. package com.crazymakercircle.demo.lock;
  2. // 省略import
  3. public class SemaphoreTest
  4. {
  5. @org.junit.Test
  6. public void testShareLock() throws InterruptedException
  7. {
  8. // 排队总人数(请求总数)
  9. final int USER_TOTAL = 10;
  10. // 可同时受理业务的窗口数量(同时并发执行的线程数)
  11. final int PERMIT_TOTAL = 2;
  12. // 线程池,用于多线程模拟测试
  13. final CountDownLatch countDownLatch =
  14. new CountDownLatch(USER_TOTAL);
  15. // 创建信号量,含有两个许可
  16. final Semaphore semaphore = new Semaphore(PERMIT_TOTAL);
  17. AtomicInteger index = new AtomicInteger(0);
  18. // 创建Runnable可执行实例
  19. Runnable r = () ->
  20. {
  21. try
  22. {
  23. //阻塞开始获取许可
  24. semaphore.acquire(1);
  25. //获取了一个许可
  26. Print.tco( DateUtil.getNowTime()
  27. + ", 受理处理中...,服务号: " + index.incrementAndGet());
  28. //模拟业务操作: 处理排队业务
  29. Thread.sleep(1000);
  30. //释放一个信号
  31. semaphore.release(1);
  32. } catch (Exception e)
  33. {
  34. e.printStackTrace();
  35. }
  36. countDownLatch.countDown();
  37. };
  38. //创建10个线程
  39. Thread[] tArray = new Thread[USER_TOTAL];
  40. for (int i = 0; i < USER_TOTAL; i++)
  41. {
  42. tArray[i] = new Thread(r, "线程" + i);
  43. }
  44. //启动10个线程
  45. for (int i = 0; i < USER_TOTAL; i++)
  46. {
  47. tArray[i].start();
  48. }
  49. countDownLatch.await();
  50. }
  51. }

5.5.3 共享锁CountDownLatch

  1. package com.crazymakercircle.visiable;
  2. // 省略import
  3. class Driver
  4. {
  5. private static final int N = 100; // 乘客数
  6. public static void main(String[] args) throws InterruptedException
  7. { //step1:创建倒数闩,设置倒数的总数
  8. CountDownLatch doneSignal = new CountDownLatch(N);
  9. //取得CPU密集型线程池
  10. Executor e = ThreadUtil.getCpuIntenseTargetThreadPool();
  11. for (int i = 1; i <= N; ++i) // 启动报数任务
  12. e.execute(new Person(doneSignal, i));
  13. doneSignal.await(); //step2:等待报数完成,倒数闩计数值为0
  14. Print.tcfo("人到齐,开车"); }
  15. static class Person implements Runnable
  16. {
  17. private final CountDownLatch doneSignal;
  18. private final int i;
  19. Person(CountDownLatch doneSignal, int i)
  20. {
  21. this.doneSignal = doneSignal;
  22. this.i = i;
  23. }
  24. public void run()
  25. {
  26. try
  27. {
  28. //报数
  29. Print.tcfo("第" + i + "个人已到");
  30. doneSignal.countDown(); //step3:倒数闩减少1
  31. } catch (Exception ex)
  32. {
  33. }
  34. }
  35. }
  36. }

5.6 读写锁

JUC包中的读写锁接口为ReadWriteLock,主要有两个方法,具体如下:

  1. public interface ReadWriteLock {
  2. /**
  3. * 返回读锁
  4. */
  5. Lock readLock();
  6. /**
  7. * 返回写锁
  8. */
  9. Lock writeLock();
  10. }

5.6.1 读写锁ReentrantReadWriteLock

接着进行代码演示,读锁是共享锁,写锁是排他锁:

  1. package com.crazymakercircle.demo.lock;
  2. // 省略import
  3. public class ReadWriteLockTest
  4. {
  5. //创建一个Map,代表共享数据
  6. final static Map<String, String> MAP = new HashMap<String, String>();
  7. //创建一个读写锁
  8. final static ReentrantReadWriteLock LOCK = new ReentrantReadWriteLock();
  9. //获取读锁
  10. final static Lock READ_LOCK = LOCK.readLock();
  11. //获取写锁
  12. final static Lock WRITE_LOCK = LOCK.writeLock();
  13. //对共享数据的写操作
  14. public static Object put(String key, String value)
  15. {
  16. WRITE_LOCK.lock(); //抢写锁
  17. try
  18. {
  19. Print.tco(DateUtil.getNowTime()

5.6.2 锁的升级与降级

锁升级是指读锁升级为写锁,锁降级指的是写锁降级为读锁。在ReentrantReadWriteLock读写锁中,只支持写锁降级为读锁,而不支持读锁升级为写锁。具体的演示代码如下:

  1. package com.crazymakercircle.demo.lock;
  2. // 省略import
  3. public class ReadWriteLockTest2
  4. {
  5. //创建一个Map,代表共享数据
  6. final static Map<String, String> MAP = new HashMap<String, String>();
  7. //创建一个读写锁
  8. final static ReentrantReadWriteLock LOCK = new ReentrantReadWriteLock();
  9. //获取读锁
  10. final static Lock READ_LOCK = LOCK.readLock();
  11. //获取写锁
  12. final static Lock WRITE_LOCK = LOCK.writeLock();
  13. //对共享数据的写操作
  14. public static Object put(String key, String value)
  15. {
  16. WRITE_LOCK.lock();
  17. try
  18. {
  19. Print.tco(DateUtil.getNowTime()
  20. + " 抢占了WRITE_LOCK,开始执行write操作");
  21. Thread.sleep(1000);
  22. String put = MAP.put(key, value);
  23. Print.tco( "尝试降级写锁为读锁");
  24. //写锁降级为读锁(成功)
  25. READ_LOCK.lock();
  26. Print.tco( "写锁降级为读锁成功");
  27. return put;
  28. } catch (Exception e)
  29. {
  30. e.printStackTrace();
  31. } finally
  32. {
  33. READ_LOCK.unlock();
  34. WRITE_LOCK.unlock();
  35. }
  36. return null;
  37. }
  38. //对共享数据的读操作
  39. public static Object get(String key)
  40. {
  41. READ_LOCK.lock();
  42. try
  43. {
  44. Print.tco(DateUtil.getNowTime()
  45. + " 抢占了READ_LOCK,开始执行read操作");
  46. Thread.sleep(1000);
  47. String value = MAP.get(key);
  48. Print.tco( "尝试升级读锁为写锁");
  49. //读锁升级为写锁(失败)
  50. WRITE_LOCK.lock();
  51. Print.tco("读锁升级为写锁成功");
  52. return value;
  53. } catch (InterruptedException e)
  54. {
  55. e.printStackTrace();
  56. } finally
  57. {
  58. WRITE_LOCK.unlock();
  59. READ_LOCK.unlock();
  60. }
  61. return null;
  62. }
  63. public static void main(String[] args)
  64. {
  65. //创建Runnable可执行实例
  66. Runnable writeTarget = () -> put("key", "value");
  67. Runnable readTarget = () -> get("key");
  68. //创建1个写线程,并启动
  69. new Thread(writeTarget, "写线程").start();
  70. //创建1个读线程
  71. new Thread(readTarget, "读线程").start();
  72. }
  73. }

5.6.3 StampedLock

StampedLock(印戳锁)是对ReentrantReadWriteLock读写锁的一种改进,主要的改进为:在没有写只有读的场景下,StampedLock支持不用加读锁而是直接进行读操作,最大程度提升读的效率,只有在发生过写操作之后,再加读锁才能进行读操作。

  1. 悲观读锁的获取与释放 ```java //获取普通读锁(悲观读锁),返回long类型的印戳值 public long readLock()

//释放普通读锁(悲观读锁),以取锁时的印戳值作为参数 public void unlockRead(long stamp)

  1. 2. 写锁的获取与释放
  2. ```java
  3. //获取写锁,返回long类型的印戳值
  4. public long writeLock()
  5. //释放写锁,以获取写锁时的印戳值作为参数
  6. public void unlockWrite(long stamp)
  1. 乐观读的印戳获取与有效性判断 ```java //获取乐观读,返回long类型的印戳值,返回0表示当前锁处于写锁模式,不能乐观读 public long tryOptimisticRead()

//判断乐观读的印戳值是否有效,以tryOptimisticRead返回的印戳值作为参数 public long tryOptimisticRead(long stamp)

  1. StampedLock的演示案例
  2. ```java
  3. package com.crazymakercircle.demo.lock;
  4. // 省略import
  5. public class StampedLockTest
  6. {
  7. //创建一个Map,代表共享数据
  8. final static Map<String, String> MAP = new HashMap<String, String>();
  9. //创建一个印戳锁
  10. final static StampedLock STAMPED_LOCK = new StampedLock();
  11. //对共享数据的写操作
  12. public static Object put(String key, String value)
  13. {
  14. long stamp = STAMPED_LOCK.writeLock(); //尝试获取写锁的印戳
  15. try
  16. {
  17. Print.tco(getNowTime() + " 抢占了WRITE_LOCK,开始执行write操作");
  18. Thread.sleep(1000);
  19. String put = MAP.put(key, value);
  20. return put;
  21. } catch (Exception e)
  22. {
  23. e.printStackTrace();
  24. } finally
  25. {
  26. Print.tco(getNowTime() + " 释放了WRITE_LOCK");
  27. STAMPED_LOCK.unlockWrite(stamp); //释放写锁
  28. }
  29. return null;
  30. }
  31. //对共享数据的悲观读操作
  32. public static Object pessimisticRead(String key)
  33. {
  34. Print.tco(getNowTime() + "LOCK进入过写模式,只能悲观读");
  35. //进入了写锁模式,只能获取悲观读锁
  36. long stamp = STAMPED_LOCK.readLock(); //尝试获取读锁的印戳
  37. try
  38. {
  39. //成功获取到读锁,并重新获取最新的变量值
  40. Print.tco(getNowTime() + " 抢占了READ_LOCK");
  41. String value = MAP.get(key);
  42. return value;
  43. } finally
  44. {
  45. Print.tco(getNowTime() + " 释放了READ_LOCK");
  46. STAMPED_LOCK.unlockRead(stamp); //释放读锁
  47. }
  48. }
  49. //对共享数据的乐观读操作
  50. public static Object optimisticRead(String key)
  51. {
  52. String value = null;
  53. //尝试进行乐观读
  54. long stamp = STAMPED_LOCK.tryOptimisticRead();
  55. if (0 != stamp)
  56. {
  57. Print.tco(getNowTime() + "乐观读的印戳值,获取成功");
  58. sleepSeconds(1); //模拟耗费时间1秒
  59. value = MAP.get(key);
  60. } else // 0 == stamp 表示当前为写锁模式
  61. {
  62. Print.tco(getNowTime() + "乐观读的印戳值,获取失败");
  63. //LOCK已经进入写模式,使用悲观读方法
  64. return pessimisticRead(key);
  65. }
  66. //乐观读操作已经间隔了一段时间,期间可能发生写入
  67. //所以,需要验证乐观读的印戳值是否有效,即判断LOCK是否进入过写模式
  68. if (!STAMPED_LOCK.validate(stamp))
  69. {
  70. //乐观读的印戳值无效,表明写锁被占用过