当前位置:   article > 正文

Java多线程——JUC ReentrantLock使用详解一篇就够,以及使用ReentrantLock避免死锁情况的产生_at java.util.concurrent.locks.abstractqueuedsynchr

at java.util.concurrent.locks.abstractqueuedsynchronizer.acquireinterruptibl

什么是ReentrantLock

简单的来讲ReentrantLock是JUC提供的一种与synchronized关键字加锁作用类似的类

与synchronized关键字相比,ReentrantLock有如下特点:

  • 可中断(一个线程可以通过interrupt方法取消另一个线程的锁等待)
  • 可以设置竞争锁资源的超时时间
  • 可以设置公平锁(synchronized关键字释放锁资源之后,其他在关联该对象Monitor的阻塞线程将会再次竞争锁资源,并没有先来先得,后来后得的公平性)
  • 支持多个条件变量(synchronized关键字竞争失败的线程都只会进入该对象锁的Monitor的阻塞队列中,也就是没有条件可以选择)
  • 与synchronized关键字一样的是都支持重入锁

可重入

  1. package com.leolee.multithreadProgramming.juc.reentrantLock;
  2. import lombok.extern.slf4j.Slf4j;
  3. import java.util.concurrent.locks.ReentrantLock;
  4. /**
  5. * @ClassName Test
  6. * @Description: JUC ReentrantLock测试
  7. * 相对于 synchronized,ReentrantLock具有如下特点:
  8. * 1.可中断(一个线程可以取消另一个线程的锁)
  9. * 2.可以设置超时时间
  10. * 3.可以设置为公平锁
  11. * 4.支持多个条件变量(synchronized竞争失败的线程都会进入Monitor的waitList,ReentrantLock可以根据不同的条件变量进入不同的集合)
  12. * 5.与synchronized一样,都支持锁重入
  13. *
  14. *
  15. * @Author LeoLee
  16. * @Date 2020/12/7
  17. * @Version V1.0
  18. **/
  19. @Slf4j
  20. public class Test {
  21. private static final ReentrantLock reentrantLock = new ReentrantLock();
  22. //============================基本使用方法以及可重入测试============================
  23. public void normalTest() {
  24. reentrantLock.lock();
  25. try {
  26. //临界区,需要被保护的代码块
  27. log.info("main method 获得锁,开始执行");
  28. this.m1();
  29. } catch (Exception e) {
  30. e.printStackTrace();
  31. } finally {
  32. reentrantLock.unlock();
  33. }
  34. }
  35. public void m1() {
  36. reentrantLock.lock();
  37. try {
  38. //临界区,需要被保护的代码块
  39. log.info("m1 获得锁,开始执行");
  40. this.m2();
  41. } catch (Exception e) {
  42. e.printStackTrace();
  43. } finally {
  44. reentrantLock.unlock();
  45. }
  46. }
  47. public void m2() {
  48. reentrantLock.lock();
  49. try {
  50. //临界区,需要被保护的代码块
  51. log.info("m2 获得锁,开始执行");
  52. } catch (Exception e) {
  53. e.printStackTrace();
  54. } finally {
  55. reentrantLock.unlock();
  56. }
  57. }
  58. public static void main(String[] args) {
  59. Test test = new Test();
  60. test.normalTest();
  61. }
  62. }
  63. 执行结果:
  64. 20:33:20.368 [main] INFO com.leolee.multithreadProgramming.juc.reentrantLock.Test - main method 获得锁,开始执行
  65. 20:33:20.388 [main] INFO com.leolee.multithreadProgramming.juc.reentrantLock.Test - m1 获得锁,开始执行
  66. 20:33:20.388 [main] INFO com.leolee.multithreadProgramming.juc.reentrantLock.Test - m2 获得锁,开始执行

可中断

可中断的特性就是为了避免线程“死等”的问题,让线程的等待受开发者控制。

因为这个中断线程等待的操作是需要其他线程来调用interrput方法,所以这种特性是一种被动的防止死锁的方式,开发者要考虑在什么情况下,何时来打断等待的线程,所以这种方式在使用上会有一些额外的考虑因素影响实际的代码编写。

  1. package com.leolee.multithreadProgramming.juc.reentrantLock;
  2. import lombok.extern.slf4j.Slf4j;
  3. import java.util.concurrent.locks.ReentrantLock;
  4. /**
  5. * @ClassName Test
  6. * @Description: JUC ReentrantLock测试
  7. * 相对于 synchronized,ReentrantLock具有如下特点:
  8. * 1.可中断(一个线程可以取消另一个线程的锁)
  9. * 2.可以设置超时时间
  10. * 3.可以设置为公平锁
  11. * 4.支持多个条件变量(synchronized竞争失败的线程都会进入Monitor的waitList,ReentrantLock可以根据不同的条件变量进入不同的集合)
  12. * 5.与synchronized一样,都支持锁重入
  13. *
  14. *
  15. * @Author LeoLee
  16. * @Date 2020/12/7
  17. * @Version V1.0
  18. **/
  19. @Slf4j
  20. public class Test {
  21. private static final ReentrantLock reentrantLock = new ReentrantLock();
  22. //============================可中断特性测试============================
  23. public void testInterruptibly() {
  24. Thread t1 = new Thread(() -> {
  25. try {
  26. //如果竞争到锁就继续执行,失败就进入阻塞队列,其他线程可以使用 interrupt 打断,即:你不要继续等待了、
  27. log.info("{} 尝试获取锁", Thread.currentThread().getName());
  28. reentrantLock.lockInterruptibly();
  29. } catch (InterruptedException e) {
  30. //被interrupt打断之后,抛出InterruptedException,就代表没有获取到锁,直接return
  31. e.printStackTrace();
  32. log.info("{} 未获取到锁,直接return", Thread.currentThread().getName());
  33. return;
  34. }
  35. //获取到锁
  36. try {
  37. log.info("{} 获取到锁,并执行代码", Thread.currentThread().getName());
  38. } finally {
  39. reentrantLock.unlock();
  40. }
  41. }, "t1");
  42. //主线程先获取锁,让t1线程进入阻塞队列
  43. reentrantLock.lock();
  44. t1.start();
  45. //主线程打断t1线程的等待
  46. try {
  47. Thread.sleep(1000);
  48. } catch (InterruptedException e) {
  49. e.printStackTrace();
  50. }
  51. log.info("{} 打断 {}", Thread.currentThread().getName(), t1.getName());
  52. t1.interrupt();
  53. }
  54. public static void main(String[] args) {
  55. Test test = new Test();
  56. test.testInterruptibly();
  57. }
  58. }
  59. 执行结果:
  60. 20:34:26.728 [t1] INFO com.leolee.multithreadProgramming.juc.reentrantLock.Test - t1 尝试获取锁
  61. 20:34:27.739 [main] INFO com.leolee.multithreadProgramming.juc.reentrantLock.Test - main 打断 t1
  62. 20:34:27.740 [t1] INFO com.leolee.multithreadProgramming.juc.reentrantLock.Test - t1 未获取到锁,直接return
  63. java.lang.InterruptedException
  64. at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
  65. at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
  66. at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
  67. at com.leolee.multithreadProgramming.juc.reentrantLock.Test.lambda$testInterruptibly$0(Test.java:82)
  68. at java.lang.Thread.run(Thread.java:745)

锁超时

上面的可中断是一种被动的防止线程死等(死锁)的特性,那么锁超时就是一种主动的方式。

  1. package com.leolee.multithreadProgramming.juc.reentrantLock;
  2. import lombok.extern.slf4j.Slf4j;
  3. import java.util.concurrent.locks.ReentrantLock;
  4. /**
  5. * @ClassName Test
  6. * @Description: JUC ReentrantLock测试
  7. * 相对于 synchronized,ReentrantLock具有如下特点:
  8. * 1.可中断(一个线程可以取消另一个线程的锁)
  9. * 2.可以设置超时时间
  10. * 3.可以设置为公平锁
  11. * 4.支持多个条件变量(synchronized竞争失败的线程都会进入Monitor的waitList,ReentrantLock可以根据不同的条件变量进入不同的集合)
  12. * 5.与synchronized一样,都支持锁重入
  13. *
  14. *
  15. * @Author LeoLee
  16. * @Date 2020/12/7
  17. * @Version V1.0
  18. **/
  19. @Slf4j
  20. public class Test {
  21. private static final ReentrantLock reentrantLock = new ReentrantLock();
  22. //============================锁超时============================
  23. public void testLockTimeout() {
  24. Thread t1 = new Thread(() -> {
  25. log.info("{} 尝试获得锁", Thread.currentThread().getName());
  26. //不带参数方法是立刻返回加锁是否成功的结果,带参的是等待一定时间后返回结果,如果在超时时间内获取到锁,则直接返回true
  27. if (!reentrantLock.tryLock()) {
  28. log.info("{} 尝试获取锁失败,直接返回", Thread.currentThread().getName());
  29. return;
  30. }
  31. //临界区代码
  32. try {
  33. //获得到了锁
  34. log.info("{} 获取锁成功,开始执行临界区代码", Thread.currentThread().getName());
  35. } finally {
  36. reentrantLock.unlock();
  37. }
  38. }, "t1");
  39. //主线程获取到锁
  40. reentrantLock.lock();
  41. t1.start();
  42. }
  43. public static void main(String[] args) {
  44. Test test = new Test();
  45. test.testLockTimeout();
  46. }
  47. }
  48. 执行结果:
  49. 21:17:11.498 [t1] INFO com.leolee.multithreadProgramming.juc.reentrantLock.Test - t1 尝试获得锁
  50. 21:17:11.517 [t1] INFO com.leolee.multithreadProgramming.juc.reentrantLock.Test - t1 尝试获取锁失败,直接返回

从执行结果看出,不带参的tryLock()获取锁的时候,几乎与同时,就返回了获取锁的结果,不会造成线程的无线等待。

带参的tryLock()如下

  1. package com.leolee.multithreadProgramming.juc.reentrantLock;
  2. import lombok.extern.slf4j.Slf4j;
  3. import java.util.concurrent.TimeUnit;
  4. import java.util.concurrent.locks.ReentrantLock;
  5. /**
  6. * @ClassName Test
  7. * @Description: JUC ReentrantLock测试
  8. * 相对于 synchronized,ReentrantLock具有如下特点:
  9. * 1.可中断(一个线程可以取消另一个线程的锁)
  10. * 2.可以设置超时时间
  11. * 3.可以设置为公平锁
  12. * 4.支持多个条件变量(synchronized竞争失败的线程都会进入Monitor的waitList,ReentrantLock可以根据不同的条件变量进入不同的集合)
  13. * 5.与synchronized一样,都支持锁重入
  14. *
  15. *
  16. * @Author LeoLee
  17. * @Date 2020/12/7
  18. * @Version V1.0
  19. **/
  20. @Slf4j
  21. public class Test {
  22. private static final ReentrantLock reentrantLock = new ReentrantLock();
  23. //============================锁超时============================
  24. public void testLockTimeout2() {
  25. Thread t1 = new Thread(() -> {
  26. log.info("{} 尝试获得锁", Thread.currentThread().getName());
  27. //不带参数方法是立刻返回加锁是否成功的结果,带参的是等待一定时间后返回结果,如果在超时时间内获取到锁,则直接返回true
  28. try {
  29. if (!reentrantLock.tryLock(2, TimeUnit.SECONDS)) {
  30. log.info("{} 尝试获取锁失败,直接返回", Thread.currentThread().getName());
  31. return;
  32. }
  33. } catch (InterruptedException e) {
  34. log.info("{} 尝试获取锁被打断,直接返回", Thread.currentThread().getName());
  35. e.printStackTrace();
  36. return;
  37. }
  38. //临界区代码
  39. try {
  40. //获得到了锁
  41. log.info("{} 获取锁成功,开始执行临界区代码", Thread.currentThread().getName());
  42. } finally {
  43. reentrantLock.unlock();
  44. }
  45. }, "t1");
  46. //主线程获取到锁
  47. reentrantLock.lock();
  48. t1.start();
  49. }
  50. public static void main(String[] args) {
  51. Test test = new Test();
  52. test.testLockTimeout2();
  53. }
  54. }
  55. 执行结果:
  56. 21:22:31.956 [t1] INFO com.leolee.multithreadProgramming.juc.reentrantLock.Test - t1 尝试获得锁
  57. 21:22:33.974 [t1] INFO com.leolee.multithreadProgramming.juc.reentrantLock.Test - t1 尝试获取锁失败,直接返回

tryLock阻塞了两秒钟来尝试获取锁资源,如果在这两秒钟之内主线程释放了锁,那么t1线程将会获得锁对象。

ReentrantLock支持公平锁

ReentrantLock默认是非公平锁,也就是当一个线程释放了锁资源之后,其他等待的线程要再同时竞争锁资源,而不是先来后到的顺序。

ReentrantLock的带参构造方法如下:

这里可以看出如果fair为true则创建的是FairSync,如果是false则是NonFairSync,默认是NonFairSync。字面意思很明显,FairSync公平锁,NonFairSync非公平锁

看一下FairSync的类图

FairSync继承了同为ReentrantLock内部类的Sync,Sync继承了AbstractQueuedSynchronizer。

详细的源码解释暂且不谈之后会有专门的文章来详解,直接看一下AbstractQueuedSynchronizer类。

  1. package java.util.concurrent.locks;
  2. import java.util.concurrent.TimeUnit;
  3. import java.util.ArrayList;
  4. import java.util.Collection;
  5. import java.util.Date;
  6. import sun.misc.Unsafe;
  7. public abstract class AbstractQueuedSynchronizer
  8. extends AbstractOwnableSynchronizer
  9. implements java.io.Serializable {
  10. private static final long serialVersionUID = 7373984972572414691L;
  11. /**
  12. * Creates a new {@code AbstractQueuedSynchronizer} instance
  13. * with initial synchronization state of zero.
  14. */
  15. protected AbstractQueuedSynchronizer() { }
  16. static final class Node {
  17. /** Marker to indicate a node is waiting in shared mode */
  18. static final Node SHARED = new Node();
  19. /** Marker to indicate a node is waiting in exclusive mode */
  20. static final Node EXCLUSIVE = null;
  21. /** waitStatus value to indicate thread has cancelled */
  22. static final int CANCELLED = 1;
  23. /** waitStatus value to indicate successor's thread needs unparking */
  24. static final int SIGNAL = -1;
  25. /** waitStatus value to indicate thread is waiting on condition */
  26. static final int CONDITION = -2;
  27. /**
  28. * waitStatus value to indicate the next acquireShared should
  29. * unconditionally propagate
  30. */
  31. static final int PROPAGATE = -3;
  32. /**
  33. * Status field, taking on only the values:
  34. * SIGNAL: The successor of this node is (or will soon be)
  35. * blocked (via park), so the current node must
  36. * unpark its successor when it releases or
  37. * cancels. To avoid races, acquire methods must
  38. * first indicate they need a signal,
  39. * then retry the atomic acquire, and then,
  40. * on failure, block.
  41. * CANCELLED: This node is cancelled due to timeout or interrupt.
  42. * Nodes never leave this state. In particular,
  43. * a thread with cancelled node never again blocks.
  44. * CONDITION: This node is currently on a condition queue.
  45. * It will not be used as a sync queue node
  46. * until transferred, at which time the status
  47. * will be set to 0. (Use of this value here has
  48. * nothing to do with the other uses of the
  49. * field, but simplifies mechanics.)
  50. * PROPAGATE: A releaseShared should be propagated to other
  51. * nodes. This is set (for head node only) in
  52. * doReleaseShared to ensure propagation
  53. * continues, even if other operations have
  54. * since intervened.
  55. * 0: None of the above
  56. *
  57. * The values are arranged numerically to simplify use.
  58. * Non-negative values mean that a node doesn't need to
  59. * signal. So, most code doesn't need to check for particular
  60. * values, just for sign.
  61. *
  62. * The field is initialized to 0 for normal sync nodes, and
  63. * CONDITION for condition nodes. It is modified using CAS
  64. * (or when possible, unconditional volatile writes).
  65. */
  66. volatile int waitStatus;
  67. /**
  68. * Link to predecessor node that current node/thread relies on
  69. * for checking waitStatus. Assigned during enqueuing, and nulled
  70. * out (for sake of GC) only upon dequeuing. Also, upon
  71. * cancellation of a predecessor, we short-circuit while
  72. * finding a non-cancelled one, which will always exist
  73. * because the head node is never cancelled: A node becomes
  74. * head only as a result of successful acquire. A
  75. * cancelled thread never succeeds in acquiring, and a thread only
  76. * cancels itself, not any other node.
  77. */
  78. volatile Node prev;
  79. /**
  80. * Link to the successor node that the current node/thread
  81. * unparks upon release. Assigned during enqueuing, adjusted
  82. * when bypassing cancelled predecessors, and nulled out (for
  83. * sake of GC) when dequeued. The enq operation does not
  84. * assign next field of a predecessor until after attachment,
  85. * so seeing a null next field does not necessarily mean that
  86. * node is at end of queue. However, if a next field appears
  87. * to be null, we can scan prev's from the tail to
  88. * double-check. The next field of cancelled nodes is set to
  89. * point to the node itself instead of null, to make life
  90. * easier for isOnSyncQueue.
  91. */
  92. volatile Node next;
  93. /**
  94. * The thread that enqueued this node. Initialized on
  95. * construction and nulled out after use.
  96. */
  97. volatile Thread thread;
  98. /**
  99. * Link to next node waiting on condition, or the special
  100. * value SHARED. Because condition queues are accessed only
  101. * when holding in exclusive mode, we just need a simple
  102. * linked queue to hold nodes while they are waiting on
  103. * conditions. They are then transferred to the queue to
  104. * re-acquire. And because conditions can only be exclusive,
  105. * we save a field by using special value to indicate shared
  106. * mode.
  107. */
  108. Node nextWaiter;
  109. /**
  110. * Returns true if node is waiting in shared mode.
  111. */
  112. final boolean isShared() {
  113. return nextWaiter == SHARED;
  114. }
  115. /**
  116. * Returns previous node, or throws NullPointerException if null.
  117. * Use when predecessor cannot be null. The null check could
  118. * be elided, but is present to help the VM.
  119. *
  120. * @return the predecessor of this node
  121. */
  122. final Node predecessor() throws NullPointerException {
  123. Node p = prev;
  124. if (p == null)
  125. throw new NullPointerException();
  126. else
  127. return p;
  128. }
  129. Node() { // Used to establish initial head or SHARED marker
  130. }
  131. Node(Thread thread, Node mode) { // Used by addWaiter
  132. this.nextWaiter = mode;
  133. this.thread = thread;
  134. }
  135. Node(Thread thread, int waitStatus) { // Used by Condition
  136. this.waitStatus = waitStatus;
  137. this.thread = thread;
  138. }
  139. }
  140. /**
  141. * Head of the wait queue, lazily initialized. Except for
  142. * initialization, it is modified only via method setHead. Note:
  143. * If head exists, its waitStatus is guaranteed not to be
  144. * CANCELLED.
  145. */
  146. private transient volatile Node head;
  147. /**
  148. * Tail of the wait queue, lazily initialized. Modified only via
  149. * method enq to add new wait node.
  150. */
  151. private transient volatile Node tail;
  152. /**
  153. * The synchronization state.
  154. */
  155. private volatile int state;
  156. /**
  157. * Returns the current value of synchronization state.
  158. * This operation has memory semantics of a {@code volatile} read.
  159. * @return current state value
  160. */
  161. protected final int getState() {
  162. return state;
  163. }
  164. /**
  165. * Sets the value of synchronization state.
  166. * This operation has memory semantics of a {@code volatile} write.
  167. * @param newState the new state value
  168. */
  169. protected final void setState(int newState) {
  170. state = newState;
  171. }
  172. //此处省略了很多方法
  173. }

看了一个final修饰的静态内部类 Node,Node中有被volatile修饰的Thread属性,以及如下两个外部类属性声明:

  1. /**
  2. * Head of the wait queue, lazily initialized. Except for
  3. * initialization, it is modified only via method setHead. Note:
  4. * If head exists, its waitStatus is guaranteed not to be
  5. * CANCELLED.
  6. */
  7. private transient volatile Node head;
  8. /**
  9. * Tail of the wait queue, lazily initialized. Modified only via
  10. * method enq to add new wait node.
  11. */
  12. private transient volatile Node tail;

这是一个太明显的提示了,说明公平锁的等待线程会以链表的方式进行排序,每次锁资源的释放,都会从链表中按照顺序取出一个最先被加入的线程来获取锁资源。

多条件变量

synchronized其实也有条件变量:当获得锁的线程在临界区中由于不满足某些条件判断,调用了锁对象的wait方法,该线程就会进入锁对象对应的Monitor中的waitSet中等待被唤醒。

而ReentrantLock相对于synchronized来说支持多个条件变量,如果说synchronized对应的waitSet是一个休息区,那么ReentrantLock就支持多个休息区。所以唤醒的时候ReentrantLock可以根据不同的休息区来分别唤醒。这样就可以一定程度的避免虚假唤醒所有等待的线程后,发现很多线程还是不满足条件再次进入wairSet。

下面是主要的使用方法

  1. public void testCondition() {
  2. //创建一个新的条件变量(即:一个休息区)
  3. Condition condition1 = reentrantLock.newCondition();
  4. Condition condition2 = reentrantLock.newCondition();
  5. reentrantLock.lock();
  6. //当某些条件不满足进入休息室等待,此处省略条件判断
  7. try {
  8. condition1.await();
  9. condition1.await(1, TimeUnit.SECONDS);//也是可以设置等待超时时间
  10. condition1.awaitUninterruptibly();//不能被打断的等待
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }
  14. //唤醒condition1中某一个等待的线程
  15. condition1.signal();
  16. //唤醒condition1中所有等待的线程
  17. condition1.signalAll();
  18. //最后要释放锁
  19. }

使用规范:

  • await前要获得锁
  • await执行后,当前线程释放锁,进入指定的条件变量中等待
  • await的唤醒(超时、被打断),线程将重新竞争锁资源
  • await之后唤醒的线程,将继续执行后续的代码
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/249250
推荐阅读
相关标签
  

闽ICP备14008679号