10. ReentrantLock重入锁

juc包下的锁,和 synchronized 相比具有的的特点

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁 (先到先得)
  • 支持多个条件变量( 具有多个 WaitSet)
  • 在对象级别保护临界区(synchronized在jvm级别保护临界区)
  • ReentrantLock锁取代了 原先的对象+Monitor锁

与synchronized一样,都支持可重入

  1. // 获取ReentrantLock对象
  2. private ReentrantLock lock = new ReentrantLock();
  3. // 加锁
  4. lock.lock();
  5. try {
  6. // 需要执行的代码
  7. }finally {
  8. // 释放锁
  9. lock.unlock();
  10. }

10.1 可重入

  • 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
  • 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住 ```java @Slf4j(topic = “c.Test12”) public class Test12 { private static ReentrantLock lock= new ReentrantLock();

    public static void main(String[] args) {

    1. lock.lock();
    2. try {
    3. log.debug("进入main");
    4. m1();
    5. }finally {
    6. lock.unlock();
    7. }

    }

    public static void m1(){

    1. lock.lock();
    2. try {
    3. log.debug("进入m1");
    4. m2();
    5. }finally {
    6. lock.unlock();
    7. }

    }

    public static void m2(){

    1. lock.lock();
    2. try {
    3. log.debug("进入m2");
    4. }finally {
    5. lock.unlock();
    6. }

    } }

  1. ```java
  2. 15:17:00.947 [main] DEBUG c.Test12 - 进入main
  3. 15:17:00.950 [main] DEBUG c.Test12 - 进入m1
  4. 15:17:00.950 [main] DEBUG c.Test12 - 进入m2

顺利进入m1, m2

10.2 可打断

  • 如果某个线程处于阻塞状态,可以调用其 interrupt 方法让其停止阻塞,获得锁失败简而言之就是:处于阻塞状态的线程,被打断了就不用阻塞了,直接停止运行 。
  • 避免死等(被动的死等,需要别的线程帮助),减少死锁发生。
  • lockInterruptibly方法才能实现,lock方法不能被打断。

    没有被打断

  1. @Slf4j(topic = "c.Test12")
  2. public class Test12 {
  3. private static ReentrantLock lock= new ReentrantLock();
  4. public static void main(String[] args) {
  5. new Thread(()->{
  6. try {
  7. log.debug("尝试获取锁");
  8. // 如果没有竞争,此方法就会获取lock对象的锁
  9. // 如果有竞争,就进入阻塞队列。
  10. // 区别就是可以被其他线程用 interrupt 方法打断
  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").start();
  23. }
  24. }
  1. 15:23:01.924 [t1] DEBUG c.Test12 - 尝试获取锁
  2. 15:23:01.927 [t1] DEBUG c.Test12 - 获取到锁

被打断了

  1. @Slf4j(topic = "c.Test12")
  2. public class Test12 {
  3. private static ReentrantLock lock= new ReentrantLock();
  4. public static void main(String[] args) {
  5. Thread t1 = new Thread(()->{
  6. try {
  7. log.debug("尝试获取锁");
  8. // 如果没有竞争,此方法就会获取lock对象的锁
  9. // 如果有竞争,就进入阻塞队列。
  10. // 区别就是可以被其他线程用 interrupt 方法打断
  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. // 主线程提前获取到锁,让t1进入阻塞队列
  24. lock.lock();
  25. t1.start();
  26. try {
  27. // 睡一秒后打断t1
  28. Thread.sleep(1000);
  29. log.debug("打断t1");
  30. t1.interrupt();
  31. } catch (InterruptedException e) {
  32. e.printStackTrace();
  33. }
  34. }
  35. }
  1. 15:25:47.668 [t1] DEBUG c.Test12 - 尝试获取锁
  2. 15:25:48.667 [main] DEBUG c.Test12 - 打断t1
  3. 15:25:48.669 [t1] DEBUG c.Test12 - 被打断了,没有获取到锁,返回
  4. java.lang.InterruptedException
  5. at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
  6. at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
  7. at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
  8. at com.ll.ch3.Test12.lambda$main$0(Test12.java:18)
  9. at java.lang.Thread.run(Thread.java:748)

10.3 锁超时

  • 使用 lock.tryLock 方法会返回获取锁是否成功。如果成功则返回 true ,反之则返回 false 。并且 tryLock 方法可以指定等待时间,参数为:tryLock(long timeout, TimeUnit unit), 其中 timeout 为最长等待时间,TimeUnit 为时间单位
  • 简而言之就是:获取锁失败了、获取超时了或者被打断了,不再阻塞,直接停止运行。
  • 主动避免死等

    不设置等待时间

  1. @Slf4j(topic = "c.Test13")
  2. public class Test13 {
  3. private static ReentrantLock lock = new ReentrantLock();
  4. public static void main(String[] args) {
  5. Thread t1 = new Thread(()->{
  6. log.debug("尝试获取锁");
  7. if (! lock.tryLock()){
  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. }
  21. }
  1. 15:35:24.703 [main] DEBUG c.Test13 - 获取到锁
  2. 15:35:24.706 [t1] DEBUG c.Test13 - 尝试获取锁
  3. 15:35:24.706 [t1] DEBUG c.Test13 - 获取不到锁

带参数,设置等待时间

  1. @Slf4j(topic = "c.Test13")
  2. public class Test13 {
  3. private static ReentrantLock lock = new ReentrantLock();
  4. public static void main(String[] args) {
  5. Thread t1 = new Thread(()->{
  6. log.debug("尝试获取锁");
  7. try {
  8. // 尝试等待1s,如果1s后还得不到锁就返回,支持别的线程打断
  9. if (! lock.tryLock(1, TimeUnit.SECONDS)){
  10. log.debug("获取不到锁");
  11. return;
  12. }
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. log.debug("被打断了,获取不到锁");
  16. return;
  17. }
  18. try {
  19. log.debug("获得到锁");
  20. }finally {
  21. lock.unlock();
  22. }
  23. },"t1");
  24. lock.lock();
  25. log.debug("获取到锁");
  26. t1.start();
  27. }
  28. }
  1. 15:37:46.115 [main] DEBUG c.Test13 - 获取到锁
  2. 15:37:46.118 [t1] DEBUG c.Test13 - 尝试获取锁
  3. 15:37:47.123 [t1] DEBUG c.Test13 - 获取不到锁

等待了1s,就结束了。

  1. @Slf4j(topic = "c.Test13")
  2. public class Test13 {
  3. private static ReentrantLock lock = new ReentrantLock();
  4. public static void main(String[] args) {
  5. Thread t1 = new Thread(()->{
  6. log.debug("尝试获取锁");
  7. try {
  8. // 尝试等待1s,如果1s后还得不到锁就返回,支持别的线程打断
  9. if (! lock.tryLock(2, TimeUnit.SECONDS)){
  10. log.debug("获取不到锁");
  11. return;
  12. }
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. log.debug("被打断了,获取不到锁");
  16. return;
  17. }
  18. try {
  19. log.debug("获得到锁");
  20. }finally {
  21. lock.unlock();
  22. }
  23. },"t1");
  24. lock.lock();
  25. log.debug("获取到锁");
  26. t1.start();
  27. try {
  28. Thread.sleep(1000);
  29. lock.unlock();
  30. log.debug("释放了锁");
  31. } catch (InterruptedException e) {
  32. e.printStackTrace();
  33. }
  34. }
  35. }
  1. 15:41:10.443 [main] DEBUG c.Test13 - 获取到锁
  2. 15:41:10.449 [t1] DEBUG c.Test13 - 尝试获取锁
  3. 15:41:11.451 [main] DEBUG c.Test13 - 释放了锁
  4. 15:41:11.451 [t1] DEBUG c.Test13 - 获得到锁

等待2s,然后主线程1s释放了锁,t1就能获得了。

解决哲学家进餐问题

让筷子继承ReentrantLock即可。

  1. @Slf4j(topic = "c.Test11")
  2. public class Test11 {
  3. public static void main(String[] args) {
  4. Chopstick c1 = new Chopstick("1");
  5. Chopstick c2 = new Chopstick("2");
  6. Chopstick c3 = new Chopstick("3");
  7. Chopstick c4 = new Chopstick("4");
  8. Chopstick c5 = new Chopstick("5");
  9. new Philosopher("哲学家1", c1, c2).start();
  10. new Philosopher("哲学家2", c2, c3).start();
  11. new Philosopher("哲学家3", c3, c4).start();
  12. new Philosopher("哲学家4", c4, c5).start();
  13. new Philosopher("哲学家5", c5, c1).start();
  14. }
  15. }
  16. // 让筷子继承ReentrantLock
  17. class Chopstick extends ReentrantLock {
  18. String name;
  19. public Chopstick(String name) {
  20. this.name = name;
  21. }
  22. @Override
  23. public String toString() {
  24. return "筷子{" +
  25. "name='" + name + '\'' +
  26. '}';
  27. }
  28. }
  29. @Slf4j(topic = "c.Philosopher")
  30. class Philosopher extends Thread {
  31. Chopstick left;
  32. Chopstick right;
  33. public Philosopher(String name, Chopstick left, Chopstick right) {
  34. super(name);
  35. this.left = left;
  36. this.right = right;
  37. }
  38. @Override
  39. public void run() {
  40. while (true) {
  41. // 尝试左手拿筷子,获取不到就释放自己手里的左手筷子
  42. if (left.tryLock()) {
  43. try {
  44. //尝试右手拿筷子,获取不到就释放自己手里的左手筷子
  45. if (right.tryLock()) {
  46. try {
  47. eat();
  48. } finally {
  49. right.unlock();
  50. }
  51. }
  52. } finally {
  53. left.unlock();
  54. }
  55. }
  56. }
  57. }
  58. private void eat() {
  59. log.debug("eat");
  60. try {
  61. Thread.sleep(1000);
  62. } catch (InterruptedException e) {
  63. e.printStackTrace();
  64. }
  65. }
  1. 15:48:04.590 [哲学家3] DEBUG c.Philosopher - eat
  2. 15:48:05.593 [哲学家3] DEBUG c.Philosopher - eat
  3. 15:48:05.593 [哲学家5] DEBUG c.Philosopher - eat
  4. 15:48:06.598 [哲学家5] DEBUG c.Philosopher - eat
  5. 15:48:06.598 [哲学家3] DEBUG c.Philosopher - eat
  6. 15:48:07.602 [哲学家2] DEBUG c.Philosopher - eat
  7. 15:48:07.601 [哲学家5] DEBUG c.Philosopher - eat
  8. 15:48:08.606 [哲学家5] DEBUG c.Philosopher - eat
  9. 15:48:08.606 [哲学家2] DEBUG c.Philosopher - eat
  10. 15:48:09.606 [哲学家2] DEBUG c.Philosopher - eat
  11. 15:48:09.606 [哲学家4] DEBUG c.Philosopher - eat
  12. 15:48:10.607 [哲学家4] DEBUG c.Philosopher - eat
  13. 15:48:10.607 [哲学家1] DEBUG c.Philosopher - eat
  14. 15:48:11.608 [哲学家1] DEBUG c.Philosopher - eat
  15. 15:48:11.608 [哲学家4] DEBUG c.Philosopher - eat
  16. 15:48:12.609 [哲学家1] DEBUG c.Philosopher - eat
  17. 15:48:12.609 [哲学家3] DEBUG c.Philosopher - eat
  18. 15:48:13.610 [哲学家5] DEBUG c.Philosopher - eat
  19. 15:48:13.610 [哲学家2] DEBUG c.Philosopher - eat

10.4 公平锁

在线程获取锁失败,进入阻塞队列时,先进入的会在锁被释放后先获得锁。这样的获取方式就是公平的。
ReentrantLock默认不公平

  • 带参数的构造方法:

    1. public ReentrantLock(boolean fair) {
    2. sync = fair ? new FairSync() : new NonfairSync();
    3. }
    1. // 默认是不公平锁,需要在创建时指定为公平锁
    2. ReentrantLock lock = new ReentrantLock(true);

    公平锁一般没有必要,会降低并发度。

    10.5 条件变量

  • synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入waitSet 等待。

  • ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比
    • synchronized 使那些不满足条件的线程都在一间休息室等消息
    • 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
  • 使用要点:
    • await 前需要获得锁
    • await 执行后,会释放锁,进入 conditionObject 等待
    • await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
    • 竞争 lock 锁成功后,从 await 后继续执行(和wait和notify方法类似)

      送烟送外卖例子改写

  1. @Slf4j(topic = "c.Test15")
  2. public class Test15 {
  3. private final static Object room = new Object();
  4. private static boolean hasCigarette = false;
  5. private static boolean hasToke = false;
  6. private static ReentrantLock ROOM = new ReentrantLock();
  7. // 等烟休息室
  8. private static Condition waitCigarette = ROOM.newCondition();
  9. // 等外卖休息室
  10. private static Condition waitToke = ROOM.newCondition();
  11. public static void main(String[] args) {
  12. new Thread(() -> {
  13. ROOM.lock();
  14. try {
  15. log.debug("有烟没?{}", hasCigarette);
  16. while (!hasCigarette) {
  17. log.debug("没烟,先歇会");
  18. try {
  19. waitCigarette.await();
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. }
  24. log.debug("可以开始干活了");
  25. } finally {
  26. ROOM.unlock();
  27. }
  28. }, "小红").start();
  29. new Thread(() -> {
  30. ROOM.lock();
  31. try {
  32. log.debug("外卖送到没?{}", hasToke);
  33. while (!hasToke) {
  34. log.debug("没外卖,先歇会");
  35. try {
  36. //Thread.sleep(2000);
  37. waitToke.await();
  38. } catch (InterruptedException e) {
  39. e.printStackTrace();
  40. }
  41. }
  42. log.debug("可以开始干活了");
  43. } finally {
  44. ROOM.unlock();
  45. }
  46. }, "小蓝").start();
  47. try {
  48. Thread.sleep(1000);
  49. } catch (InterruptedException e) {
  50. e.printStackTrace();
  51. }
  52. new Thread(() -> {
  53. ROOM.lock();
  54. try {
  55. log.debug("外卖送到了");
  56. hasToke = true;
  57. // 唤醒等外卖的休息室
  58. waitToke.signal();
  59. }finally {
  60. ROOM.unlock();
  61. }
  62. }, "送外卖").start();
  63. try {
  64. Thread.sleep(1000);
  65. } catch (InterruptedException e) {
  66. e.printStackTrace();
  67. }
  68. new Thread(() -> {
  69. ROOM.lock();
  70. try {
  71. log.debug("烟送到了");
  72. hasCigarette = true;
  73. // 唤醒等烟的休息室
  74. waitCigarette.signal();
  75. }finally {
  76. ROOM.unlock();
  77. }
  78. }, "送烟").start();
  79. }
  80. }
  1. 16:11:02.893 [小红] DEBUG c.Test15 - 有烟没?false
  2. 16:11:02.897 [小红] DEBUG c.Test15 - 没烟,先歇会
  3. 16:11:02.898 [小蓝] DEBUG c.Test15 - 外卖送到没?false
  4. 16:11:02.898 [小蓝] DEBUG c.Test15 - 没外卖,先歇会
  5. 16:11:03.893 [送外卖] DEBUG c.Test15 - 外卖送到了
  6. 16:11:03.894 [小蓝] DEBUG c.Test15 - 可以开始干活了
  7. 16:11:04.896 [送烟] DEBUG c.Test15 - 烟送到了
  8. 16:11:04.897 [小红] DEBUG c.Test15 - 可以开始干活了

11. 同步模式之顺序控制

11.1 固定运行顺序

必须先2后1

  • 方法1:wait&notify

    1. @Slf4j(topic = "c.Test16")
    2. public class Test16 {
    3. private static Object lock = new Object();
    4. // t2是否运行过
    5. private static boolean t2Run = false;
    6. public static void main(String[] args) {
    7. Thread t1 = new Thread(()->{
    8. synchronized (lock){
    9. while (!t2Run){
    10. try {
    11. lock.wait();
    12. } catch (InterruptedException e) {
    13. e.printStackTrace();
    14. }
    15. }
    16. log.debug("1");
    17. }
    18. },"t1");
    19. Thread t2 = new Thread(()->{
    20. synchronized (lock){
    21. log.debug("2");
    22. t2Run = true;
    23. lock.notify();
    24. }
    25. },"t2");
    26. t1.start();
    27. t2.start();
    28. }
    29. }
    1. 16:18:10.164 [t2] DEBUG c.Test16 - 2
    2. 16:18:10.168 [t1] DEBUG c.Test16 - 1
  • 方法2:park&unpark ```java @Slf4j(topic = “c.Test17”) public class Test17 { public static void main(String[] args) {

    1. Thread t1 = new Thread(()->{
    2. LockSupport.park();
    3. log.debug("1");
    4. },"t1");
    5. Thread t2 = new Thread(()->{
    6. log.debug("2");
    7. // 让t1恢复运行
    8. LockSupport.unpark(t1);
    9. },"t2");
    10. t1.start();
    11. t2.start();

    } }

  1. ```java
  2. 16:22:32.902 [t2] DEBUG c.Test17 - 2
  3. 16:22:32.902 [t1] DEBUG c.Test17 - 1

11.2 交替输出

线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次。现在要求输出 abcabcabcabcabc 怎么实现。

  • 使用wait&notify ```java @Slf4j(topic = “c.Test18”) public class Test18 {

    public static void main(String[] args) {

    1. NotifyWait notifyWait = new NotifyWait(1, 5);
    2. new Thread(()->{
    3. notifyWait.print("a", 1, 2);
    4. },"t1").start();
    5. new Thread(()->{
    6. notifyWait.print("b", 2, 3);
    7. },"t2").start();
    8. new Thread(()->{
    9. notifyWait.print("c", 3, 1);
    10. },"t3").start();

    } }

class NotifyWait {

  1. // 等待标记
  2. private int flag;
  3. // 循环次数
  4. private int loop;
  5. public NotifyWait(int flag, int loop) {
  6. this.flag = flag;
  7. this.loop = loop;
  8. }
  9. // 打印
  10. // 打印内容,等待标记,下一个标记
  11. public void print(String str, int waitFlag, int nextFlag){
  12. for (int i = 0; i < loop; i++) {
  13. synchronized (this){
  14. while (flag != waitFlag){
  15. try {
  16. this.wait();
  17. } catch (InterruptedException e) {
  18. e.printStackTrace();
  19. }
  20. }
  21. // 打印当前值
  22. System.out.print(str);
  23. // 更改标记
  24. flag = nextFlag;
  25. this.notifyAll();
  26. }
  27. }
  28. }

}

  1. ```java
  2. abcabcabcabcabc
  • 使用wait&notify ```java

@Slf4j(topic = “c.Test18”) public class Test18 {

  1. public static void main(String[] args) {
  2. AwaitSignal awaitSignal = new AwaitSignal(5);
  3. Condition a = awaitSignal.newCondition();
  4. Condition b = awaitSignal.newCondition();
  5. Condition c = awaitSignal.newCondition();
  6. new Thread(()->{
  7. awaitSignal.print("a", a, b);
  8. },"t1").start();
  9. new Thread(()->{
  10. awaitSignal.print("b", b, c);
  11. },"t2").start();
  12. new Thread(()->{
  13. awaitSignal.print("c", c, a);
  14. },"t3").start();
  15. // 主线程先唤醒a休息室
  16. try {
  17. Thread.sleep(1000);
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. }
  21. awaitSignal.lock();
  22. try {
  23. a.signal();
  24. }finally {
  25. awaitSignal.unlock();
  26. }
  27. }

}

class AwaitSignal extends ReentrantLock {

  1. // 循环次数
  2. private int loop;
  3. public AwaitSignal(int loop) {
  4. this.loop = loop;
  5. }
  6. // 打印
  7. // 打印内容 ,进入哪个休息室,下一间休息室
  8. public void print(String str, Condition current, Condition next){
  9. for (int i = 0; i < loop; i++) {
  10. lock();
  11. try {
  12. // 等待
  13. current.await();
  14. // 打印自己的
  15. System.out.print(str);
  16. // 叫醒下一个休息室的
  17. next.signal();
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. } finally {
  21. unlock();
  22. }
  23. }
  24. }

}

  1. ```java
  2. abcabcabcabcabc
  • 使用park&unpark ```java @Slf4j(topic = “c.Test18”) public class Test18 { public static Thread t1, t2, t3;

    public static void main(String[] args) {

    1. ParkAndUnPark obj = new ParkAndUnPark(5);
    2. t1 = new Thread(() -> {
    3. obj.run("a", t2);
    4. });
    5. t2 = new Thread(() -> {
    6. obj.run("b", t3);
    7. });
    8. t3 = new Thread(() -> {
    9. obj.run("c", t1);
    10. });
    11. t1.start();
    12. t2.start();
    13. t3.start();
    14. LockSupport.unpark(t1);

    } }

class ParkAndUnPark { public void run(String str, Thread nextThread) { for (int i = 0; i < loopNumber; i++) { LockSupport.park(); System.out.print(str); LockSupport.unpark(nextThread); } }

  1. private int loopNumber;
  2. public ParkAndUnPark(int loopNumber) {
  3. this.loopNumber = loopNumber;
  4. }

}

  1. ```java
  2. abcabcabcabcabc

12.本章小结

  • 分析多线程访问共享资源时,哪些代码片段属于临界区(对共享资源既有读,又有写)
  • 使用 synchronized 互斥解决临界区的线程安全问题
    • 掌握 synchronized 锁对象语法
    • 掌握 synchronzied 加载成员方法和静态方法语法
    • 掌握 wait/notify 同步方法
  • 使用 lock(ReentrantLock) 互斥解决临界区的线程安全问题 掌握 lock 的使用细节:可打断、锁超时、公平锁、条件变量
  • 学会分析变量的线程安全性、掌握常见线程安全类的使用
  • 了解线程活跃性问题:死锁、活锁、饥饿
  • 应用方面
    • 互斥:使用 synchronized 或 Lock 达到共享资源互斥效果,实现临界区代码的原子性效果,保证线程安全。
    • 同步:使用 wait/notify 或 Lock 的条件变量来达到线程间通信效果。
  • 原理方面
    • monitor、synchronized 、wait/notify 原理
    • synchronized 进阶原理
    • park & unpark 原理
  • 模式方面
    • 同步模式之保护性暂停
    • 异步模式之生产者消费者
    • 同步模式之顺序控制