共享带来的问题

image.png

  • 在这些时候,算盘没利用起来(不能收钱了),老王觉得有点不划算
  • 另外,小女也想用用算盘,如果总是小南占着算盘,让小女觉得不公平
  • 于是,老王灵机一动,想了个办法 [ 让他们每人用一会,轮流使用算盘 ]
  • 这样,当小南阻塞的时候,算盘可以分给小女使用,不会浪费,反之亦然
  • 最近执行的计算比较复杂,需要存储一些中间结果,而学生们的脑容量(工作内存)不够,所以老王申请了
  • 一个笔记本(主存),把一些中间结果先记在本上
  • 计算流程是这样的

image.png

  • 但是由于分时系统,有一天还是发生了事故
  • 小南刚读取了初始值 0 做了个 +1 运算,还没来得及写回结果
  • 老王说 [ 小南,你的时间到了,该别人了,记住结果走吧 ],于是小南念叨着 [ 结果是1,结果是1…] 不甘心地

到一边待着去了(上下文切换)

  • 老王说 [ 小女,该你了 ],小女看到了笔记本上还写着 0 做了一个 -1 运算,将结果 -1 写入笔记本
  • 这时小女的时间也用完了,老王又叫醒了小南:[小南,把你上次的题目算完吧],小南将他脑海中的结果 1 写

入了笔记本
image.png 小南和小女都觉得自己没做错,但笔记本里的结果是 1 而不是 0

image.png

临界区 Critical Section

  • 一个程序运行多个线程本身是没有问题的
  • 问题出在多个线程访问共享资源
    • 多个线程读共享资源其实也没有问题
    • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为 临界区
    1. static int counter = 0;
    2. static void increment()
    3. // 临界区
    4. {
    5. counter++;
    6. }
    7. static void decrement()
    8. // 临界区
    9. {
    10. counter--;
    11. }

    竞态条件 Race Condition

    多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

    synchronized 解决方案

    应用之互斥
    为了避免临界区的竞态条件发生,有多种手段可以达到目的。
    阻塞式的解决方案:synchronized,Lock
    非阻塞式的解决方案:原子变量
    本次课使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一
    时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

    注意 虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的: 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

synchronized

  1. 语法
  2. synchronized(对象) // 线程1, 线程2(blocked)
  3. {
  4. 临界区
  5. }

image.png
思考
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切
换所打断。
为了加深理解,请思考下面的问题

  • 如果把 synchronized(obj) 放在 for 循环的外面,如何理解?— 原子性
  • 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?— 锁对象
  • 如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?— 锁对象

    方法上的 synchronized

    1. class Test{
    2. public synchronized void test() {
    3. }
    4. }
    5. 等价于
    6. class Test{
    7. public void test() {
    8. synchronized(this) {
    9. }
    10. }
    11. }
    1. class Test{
    2. public synchronized static void test() {
    3. }
    4. }
    5. 等价于
    6. class Test{
    7. public static void test() {
    8. synchronized(Test.class) {
    9. }
    10. }
    11. }

    所谓的“线程八锁”

    其实就是考察 synchronized 锁住的是哪个对象

    1. @Slf4j(topic = "c.Number")
    2. class Number{
    3. public synchronized void a() {
    4. log.debug("1");
    5. }
    6. public synchronized void b() {
    7. log.debug("2");
    8. }
    9. }
    10. public static void main(String[] args) {
    11. Number n1 = new Number();
    12. new Thread(()->{ n1.a(); }).start();
    13. new Thread(()->{ n1.b(); }).start();
    14. }
    15. 12 21

    ```java @Slf4j(topic = “c.Number”) class Number{ public synchronized void a() { sleep(1); log.debug(“1”); } public synchronized void b() { log.debug(“2”); } } public static void main(String[] args) { Number n1 = new Number(); new Thread(()->{ n1.a(); }).start(); new Thread(()->{ n1.b(); }).start(); }

  1. ```java
  2. @Slf4j(topic = "c.Number")
  3. class Number{
  4. public synchronized void a() {
  5. sleep(1);
  6. log.debug("1");
  7. }
  8. public synchronized void b() {
  9. log.debug("2");
  10. }
  11. public void c() {
  12. log.debug("3");
  13. }
  14. }
  15. public static void main(String[] args) {
  16. Number n1 = new Number();
  17. new Thread(()->{ n1.a(); }).start();
  18. new Thread(()->{ n1.b(); }).start();
  19. new Thread(()->{ n1.c(); }).start();
  20. }
  21. 3 1s 12
  22. 23 1s 1
  23. 32 1s 1
  1. @Slf4j(topic = "c.Number")
  2. class Number{
  3. public synchronized void a() {
  4. sleep(1);
  5. log.debug("1");
  6. }
  7. public synchronized void b() {
  8. log.debug("2");
  9. }
  10. }
  11. public static void main(String[] args) {
  12. Number n1 = new Number();
  13. Number n2 = new Number();
  14. new Thread(()->{ n1.a(); }).start();
  15. new Thread(()->{ n2.b(); }).start();
  16. }
  1. @Slf4j(topic = "c.Number")
  2. class Number{
  3. public static synchronized void a() {
  4. sleep(1);
  5. log.debug("1");
  6. }
  7. public synchronized void b() {
  8. log.debug("2");
  9. }
  10. }
  11. public static void main(String[] args) {
  12. Number n1 = new Number();
  13. new Thread(()->{ n1.a(); }).start();
  14. new Thread(()->{ n1.b(); }).start();
  15. }
  1. @Slf4j(topic = "c.Number")
  2. class Number{
  3. public static synchronized void a() {
  4. sleep(1);
  5. log.debug("1");
  6. }
  7. public static synchronized void b() {
  8. log.debug("2");
  9. }
  10. }
  11. public static void main(String[] args) {
  12. Number n1 = new Number();
  13. new Thread(()->{ n1.a(); }).start();
  14. new Thread(()->{ n1.b(); }).start();
  15. }
  1. @Slf4j(topic = "c.Number")
  2. class Number{
  3. public static synchronized void a() {
  4. sleep(1);
  5. log.debug("1");
  6. }
  7. public synchronized void b() {
  8. log.debug("2");
  9. }
  10. }
  11. public static void main(String[] args) {
  12. Number n1 = new Number();
  13. Number n2 = new Number();
  14. new Thread(()->{ n1.a(); }).start();
  15. new Thread(()->{ n2.b(); }).start();
  16. }
  1. class Number{
  2. public static synchronized void a() {
  3. sleep(1);
  4. log.debug("1");
  5. }
  6. public static synchronized void b() {
  7. log.debug("2");
  8. }
  9. }
  10. public static void main(String[] args) {
  11. Number n1 = new Number();
  12. Number n2 = new Number();
  13. new Thread(()->{ n1.a(); }).start();
  14. new Thread(()->{ n2.b(); }).start();
  15. }

变量的线程安全分析

成员变量和静态变量是否线程安全?

  • 如果它们没有共享,则线程安全
  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
  • 如果只有读操作,则线程安全
  • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

    局部变量是否线程安全?

  • 局部变量是线程安全的

  • 但局部变量引用的对象则未必
  • 如果该对象没有逃离方法的作用访问,它是线程安全的
  • 如果该对象逃离方法的作用范围,需要考虑线程安全

image.png

private 或 final 提供【安全】的意义所在,请体会开闭原则中的【闭】

常见线程安全类

String
Integer
StringBuffer
Random
Vector
Hashtable
java.util.concurrent 包下的类

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为 它们的每个方法是原子的 但注意它们多个方法的组合不是原子的,见后面分析

  1. Hashtable table = new Hashtable();
  2. new Thread(()->{
  3. table.put("key", "value1");
  4. }).start();
  5. new Thread(()->{
  6. table.put("key", "value2");
  7. }).start();

线程安全类方法的组合

  1. Hashtable table = new Hashtable();
  2. // 线程1,线程2
  3. if( table.get("key") == null) {
  4. table.put("key", value);
  5. }

image.png

不可变类线程安全性

String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安
全的呢?
image.png

Monitor 概念

Java 对象锁

image.png
image.pngimage.png

概念

image.pngimage.png

轻量级锁

轻量级锁的使用场景:如果一个对象然有多个线程访问,单多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。

轻量级锁对使用者是透明的,即语法仍然是 synchronized

假设有两个方法同步块,利用同一个对象加锁

image.png

image.png

image.png
image.png

锁膨胀

image.png
image.png

自旋优化

image.png
image.png

image.png偏向锁

image.png
image.png
image.png

偏向状态

image.png

禁用偏向锁

image.png

调用hashcode时

会 创建hash吗 把线程id替换成hashcode码

image.png撤销 wait notify 时也会放弃偏向锁。

批量重定向

image.png

批量撤销

image.png

锁消除

JIT 对字节码优化 不会枷锁的地方不加锁

wait / notify

image.png
API 介绍

  • obj.wait() 让进入 object 监视器的线程到 waitSet 等待
  • obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
  • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒

它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法。

  • wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到

notify 为止

  • wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify

    wait notify 的正确姿势

    开始之前先看看

    sleep(long n) 和 wait(long n) 的区别

    1) sleep 是 Thread 方法,而 wait 是 Object 的方法
    2) sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
    3) sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
    4) 它们状态 TIMED_WAITING
  1. synchronized(lock) {
  2. while(条件不成立) {
  3. lock.wait();
  4. }
  5. // 干活
  6. }
  7. //另一个线程
  8. synchronized(lock) {
  9. lock.notifyAll();
  10. }

Park & Unpark

image.png
image.png
image.png

原理

image.png
image.png

image.png

image.png

重新理解线程状态转换

image.png
假设有线程 Thread t

情况 1 NEW —> RUNNABLE

  • 当调用 t.start() 方法时,由 NEW —> RUNNABLE

情况 2 RUNNABLE <—> WAITING

t 线程 用 synchronized(obj) 获取了对象锁后

  • 调用 obj.wait() 方法时,t 线程RUNNABLE —> WAITING
  • 调用 obj.notify() , obj.notifyAll() , t.interrupt() 时

    • 竞争锁成功,t 线程WAITING —> RUNNABLE
    • 竞争锁失败,t 线程WAITING —> BLOCKED ```java public class TestWaitNotify { final static Object obj = new Object(); public static void main(String[] args) { new Thread(() -> { synchronized (obj) {

      1. log.debug("执行....");
      2. try {
      3. obj.wait();
      4. } catch (InterruptedException e) {
      5. e.printStackTrace();
      6. }
      7. log.debug("其它代码...."); // 断点
      8. }

      },”t1”).start(); new Thread(() -> {

      1. synchronized (obj) {
      2. log.debug("执行....");
      3. try {
      4. obj.wait();
      5. } catch (InterruptedException e) {
      6. e.printStackTrace();
      7. }
      8. log.debug("其它代码...."); // 断点
      9. }

      },”t2”).start();

      sleep(0.5); log.debug(“唤醒 obj 上其它线程”); synchronized (obj) {

      1. obj.notifyAll(); // 唤醒obj上所有等待线程 断点

      } } }

  1. <a name="qjYOe"></a>
  2. ### 情况 3 RUNNABLE <--> WAITING
  3. - **当前线程**调用 **t.join()** 方法时,**当前线程**从 **RUNNABLE **--> **WAITING
  4. **
  5. - 注意是当前线程在**t 线程对象**的监视器上等待
  6. - **t 线程**运行结束,或调用了**当前线程**的 interrupt() 时,当前线程从 **WAITING **--> **RUNNABLE**
  7. <a name="Kc1RB"></a>
  8. ### 情况 4 RUNNABLE <--> WAITING
  9. - **当前线程**调用 **LockSupport.park()** 方法会让当前线程从 **RUNNABLE **--> **WAITING
  10. **
  11. - 调用 **LockSupport.unpark(目标线程)** 或调用了线程 的 **interrupt() **,会让**目标线程**从 **WAITING **-->
  12. **RUNNABLE
  13. **
  14. <a name="zXMcG"></a>
  15. ### 情况 5 RUNNABLE <--> TIMED_WAITING
  16. **t 线程**用 **synchronized(obj)** 获取了对象锁后
  17. - 调用** obj.wait(long n)** 方法时,t 线程从 **RUNNABLE **--> **TIMED_WAITING
  18. **
  19. - **t 线程**等待时间超过了 n 毫秒,或调用 **obj.notify()** , **obj.notifyAll()** , **t.interrupt()** 时
  20. - 竞争锁成功,**t 线程**从 **TIMED_WAITING **--> **RUNNABLE
  21. **
  22. - 竞争锁失败,**t 线程**从 **TIMED_WAITING **--> **BLOCKED **
  23. <a name="adIiw"></a>
  24. ### 情况 6 RUNNABLE <--> TIMED_WAITING
  25. - **当前线程**调用** t.join(long n)** 方法时,当前线程从 **RUNNABLE **--> **TIMED_WAITING
  26. **
  27. - 注意是**当前线程**在**t 线程对象**的**监视器**上等待
  28. - **当前线程**等待时间超过了 n 毫秒,或**t 线程**运行结束,或调用了当前线程的** interrupt()** 时,当前线程从
  29. **TIMED_WAITING **--> **RUNNABLE**
  30. <a name="dclIw"></a>
  31. ###
  32. 情况 7 RUNNABLE <--> TIMED_WAITING
  33. - **当前线程**调用 **Thread.sleep(long n)** ,**当前线程**从 **RUNNABLE **--> **TIMED_WAITING
  34. **
  35. - **当前线程**等待时间超过了 n 毫秒,**当前线程**从 **TIMED_WAITING **--> **RUNNABLE
  36. **
  37. <a name="Pm9dt"></a>
  38. ### 情况 8 RUNNABLE <--> TIMED_WAITING
  39. - 当前线程调用 **LockSupport.parkNanos(long nanos)** 或 **LockSupport.parkUntil(long millis) **时,**当前线**
  40. **程**从 **RUNNABLE** --> **TIMED_WAITING
  41. **
  42. - 调用 **LockSupport.unpark(目标线程) **或调用了线程 的** interrupt()** ,或是等待超时,会让**目标线程**从
  43. **TIMED_WAITING**--> **RUNNABLE**
  44. <a name="P8fIa"></a>
  45. ### 情况 9 RUNNABLE <--> BLOCKED
  46. - **t 线程**用 **synchronized(obj)** 获取了对象锁时如果竞争失败,从 **RUNNABLE **--> **BLOCKED
  47. **
  48. - 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 **BLOCKED **的线程重新竞争,如果其中 t 线程竞争
  49. 成功,从 **BLOCKED **--> **RUNNABLE **,其它失败的线程仍然 **BLOCKED**
  50. <a name="r1BSB"></a>
  51. ### 情况 10 RUNNABLE <--> TERMINATED
  52. 当前线程所有代码运行完毕,进入 **TERMINATED**
  53. <a name="JVHix"></a>
  54. ## 多把锁
  55. <a name="VBK6J"></a>
  56. ### 多把不相干的锁
  57. 一间大屋子有两个功能:睡觉、学习,互不相干。
  58. <br />现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低
  59. <br />解决方法是准备多个房间(多个对象锁)
  60. ```java
  61. class BigRoom {
  62. public void sleep() {
  63. synchronized (this) {
  64. log.debug("sleeping 2 小时");
  65. Sleeper.sleep(2);
  66. }
  67. }
  68. public void study() {
  69. synchronized (this) {
  70. log.debug("study 1 小时");
  71. Sleeper.sleep(1);
  72. }
  73. }
  74. }
  75. BigRoom bigRoom = new BigRoom();
  76. new Thread(() -> {
  77. bigRoom.compute();
  78. },"小南").start();
  79. new Thread(() -> {
  80. bigRoom.sleep();
  81. },"小女").start();
  82. 12:13:54.471 [小南] c.BigRoom - study 1 小时
  83. 12:13:55.476 [小女] c.BigRoom - sleeping 2 小时

改进

  1. class BigRoom {
  2. private final Object studyRoom = new Object();
  3. private final Object bedRoom = new Object();
  4. public void sleep() {
  5. synchronized (bedRoom) {
  6. log.debug("sleeping 2 小时");
  7. Sleeper.sleep(2);
  8. }
  9. }
  10. public void study() {
  11. synchronized (studyRoom) {
  12. log.debug("study 1 小时");
  13. Sleeper.sleep(1);
  14. }
  15. }
  16. }

将锁的粒度细分

  • 好处,是可以增强并发度
  • 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁

    活跃性

    死锁

    有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
    t1 线程 获得 A对象 锁,接下来想获取 B对象 的锁
    t2 线程 获得 B对象 锁,接下来想获取 A对象 的锁

    定位死锁

    检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁:
    1. cmd > jps
    2. Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
    3. 12320 Jps
    4. 22816 KotlinCompileDaemon
    5. 33200 TestDeadLock // JVM 进程
    6. 11508 Main
    7. 28468 Launcher
    ```java cmd > jstack 33200 Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8 2018-12-29 05:51:40 Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.91-b14 mixed mode): “DestroyJavaVM” #13 prio=5 os_prio=0 tid=0x0000000003525000 nid=0x2f60 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE “Thread-1” #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting for monitor entry [0x000000001f54f000] java.lang.Thread.State: BLOCKED (on object monitor) at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28)
    • waiting to lock <0x000000076b5bf1c0> (a java.lang.Object)
    • locked <0x000000076b5bf1d0> (a java.lang.Object) at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) “Thread-0” #11 prio=5 os_prio=0 tid=0x000000001eb68800 nid=0x1b28 waiting for monitor entry [0x000000001f44f000] java.lang.Thread.State: BLOCKED (on object monitor) at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15)
    • waiting to lock <0x000000076b5bf1d0> (a java.lang.Object)
      • locked <0x000000076b5bf1c0> (a java.lang.Object) at thread.TestDeadLock$$Lambda$1/495053715.run(Unknown Source) at java.lang.Thread.run(Thread.java:745)

// 略去部分输出

Found one Java-level deadlock:

“Thread-1”: waiting to lock monitor 0x000000000361d378 (object 0x000000076b5bf1c0, a java.lang.Object), which is held by “Thread-0” “Thread-0”: waiting to lock monitor 0x000000000361e768 (object 0x000000076b5bf1d0, a java.lang.Object), which is held by “Thread-1”

Java stack information for the threads listed above:

“Thread-1”: at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28)

  • waiting to lock <0x000000076b5bf1c0> (a java.lang.Object)
  • locked <0x000000076b5bf1d0> (a java.lang.Object) at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) “Thread-0”: at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15)
  • waiting to lock <0x000000076b5bf1d0> (a java.lang.Object)
  • locked <0x000000076b5bf1c0> (a java.lang.Object) at thread.TestDeadLock$$Lambda$1/495053715.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) Found 1 deadlock.
  1. - 避免死锁要注意加锁顺序
  2. - 另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到
  3. CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查
  4. <a name="adygu"></a>
  5. ### 哲学家就餐问题
  6. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/21621722/1635338618892-03da42b2-0241-4e4f-a1e8-9a1a7d4e0b09.png#clientId=ubc01d917-c74a-4&from=paste&id=u0793fcfc&margin=%5Bobject%20Object%5D&name=image.png&originHeight=795&originWidth=1205&originalType=binary&ratio=1&size=203319&status=done&style=shadow&taskId=ua3cb0198-e90a-4fbb-9ac4-85e577a17e2)
  7. <a name="L7H1c"></a>
  8. ### 活锁
  9. 活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束
  10. <a name="NmgDk"></a>
  11. ### 饥饿
  12. 很多教程中把饥饿定义为,**一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,**饥饿的情况不
  13. <br />易演示,讲读写锁时会涉及饥饿问题
  14. <br />下面我讲一下我遇到的一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/21621722/1635339318292-5eed0e92-3326-4a1e-8f18-bf087e9cec66.png#clientId=ubc01d917-c74a-4&from=paste&id=u2e51b71b&margin=%5Bobject%20Object%5D&name=image.png&originHeight=580&originWidth=1276&originalType=binary&ratio=1&size=107640&status=done&style=shadow&taskId=u14bd09d8-5e3e-44e4-ad85-ad422abea46)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/21621722/1635339376995-693a35bd-487e-4bd3-a3cd-d39e966fa544.png#clientId=ubc01d917-c74a-4&from=paste&id=u4620a5ce&margin=%5Bobject%20Object%5D&name=image.png&originHeight=563&originWidth=1122&originalType=binary&ratio=1&size=108349&status=done&style=shadow&taskId=ua398da07-518c-457f-a2b6-e01c6be9b8c)
  15. <a name="SPdQ3"></a>
  16. ## ReentrantLock
  17. 相对于 synchronized 它具备如下特点
  18. - 可中断
  19. - 可以设置超时时间
  20. - 可以设置为公平锁
  21. - 支持多个条件变量
  22. synchronized 一样,都支持可重入
  23. <br />基本语法
  24. ```java
  25. // 获取锁
  26. reentrantLock.lock();
  27. try {
  28. // 临界区
  29. } finally {
  30. // 释放锁
  31. reentrantLock.unlock();
  32. }

可重入

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

  1. static ReentrantLock lock = new ReentrantLock();
  2. public static void main(String[] args) {
  3. method1();
  4. }
  5. public static void method1() {
  6. lock.lock();
  7. try {
  8. log.debug("execute method1");
  9. method2();
  10. } finally {
  11. lock.unlock();
  12. }
  13. }
  14. public static void method2() {
  15. lock.lock();
  16. try {
  17. log.debug("execute method2");
  18. method3();
  19. } finally {
  20. lock.unlock();
  21. }
  22. }
  23. public static void method3() {
  24. lock.lock();
  25. try {
  26. log.debug("execute method3");
  27. } finally {
  28. lock.unlock();
  29. }
  30. }

image.png

可打断

  1. ReentrantLock lock = new ReentrantLock();
  2. Thread t1 = new Thread(() -> {
  3. log.debug("启动...");
  4. try {
  5. lock.lockInterruptibly();
  6. } catch (InterruptedException e) {
  7. e.printStackTrace();
  8. log.debug("等锁的过程中被打断");
  9. return;
  10. }
  11. try {
  12. log.debug("获得了锁");
  13. } finally {
  14. lock.unlock();
  15. }
  16. }, "t1");
  17. lock.lock();
  18. log.debug("获得了锁");
  19. t1.start();
  20. try {
  21. sleep(1);
  22. t1.interrupt();
  23. log.debug("执行打断");
  24. } finally {
  25. lock.unlock();
  26. }

image.png
注意如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断

  1. ReentrantLock lock = new ReentrantLock();
  2. Thread t1 = new Thread(() -> {
  3. log.debug("启动...");
  4. lock.lock();
  5. try {
  6. log.debug("获得了锁");
  7. } finally {
  8. lock.unlock();
  9. }
  10. }, "t1");
  11. lock.lock();
  12. log.debug("获得了锁");
  13. t1.start();
  14. try {
  15. sleep(1);
  16. t1.interrupt();
  17. log.debug("执行打断");
  18. sleep(1);
  19. } finally {
  20. log.debug("释放了锁");
  21. lock.unlock();
  22. }

image.png

锁超时

立刻失败

  1. ReentrantLock lock = new ReentrantLock();
  2. Thread t1 = new Thread(() -> {
  3. log.debug("启动...");
  4. if (!lock.tryLock()) {
  5. log.debug("获取立刻失败,返回");
  6. return;
  7. }
  8. try {
  9. log.debug("获得了锁");
  10. } finally {
  11. lock.unlock();
  12. }
  13. }, "t1");
  14. lock.lock();
  15. log.debug("获得了锁");
  16. t1.start();
  17. try {
  18. sleep(2);
  19. } finally {
  20. lock.unlock();
  21. }

image.png
image.png

公平锁

ReentrantLock 默认是不公平的
image.png
image.png

条件变量

synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比

  • synchronized 是那些不满足条件的线程都在一间休息室等消息
  • 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤 醒

使用要点:

  • await 前需要获得锁
  • await 执行后,会释放锁,进入 conditionObject 等待
  • await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await 后继续执行

    本章小结

    image.png