jdk 源码系列之ReentrantLock

Posted 编程的那些年

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了jdk 源码系列之ReentrantLock相关的知识,希望对你有一定的参考价值。


最近将 ReentrantLock 学了一遍同时也把源码读了一遍,记录下学习的过程

JDK 源码系列

  • jdk 源码系列之StringBuilder、StringBuffer

  • jdk 源码系列之HashMap

使用

使用锁机制,来保障线程安全

 
   
   
 
  1. Lock lock = new ReentrantLock();


  2. lock.lock();


  3. try {

  4. // 受此锁保护的资源块

  5. } finally {

  6. lock.unlock();

  7. }

或者你可以使用 tryLock() 方法,在多线程中,当一个线程释放锁的时候,就尝试去获取锁。

 
   
   
 
  1. Lock lock = new ReentrantLock();

  2. if (lock.tryLock()) {

  3. try {

  4. // 受此锁保护的资源块

  5. } finally {

  6. lock.unlock();

  7. }

  8. } else {

  9. // 进行其他操作,未被锁保护

  10. }

这种方法,在确保获取锁的时候才会解锁,并且在未获锁时不会尝试去解锁。

如果尝试去获取锁的时候太长,也可以给获取锁的这个过程加上时间,超时则直接中断线程。

 
   
   
 
  1. public static void main(String[] args) throws InterruptedException {

  2. Lock lock = new ReentrantLock();

  3. if (lock.tryLock(5, TimeUnit.SECONDS)) {

  4. try {

  5. // manipulate protected state


  6. }

  7. finally {

  8. lock.unlock();

  9. }

  10. } else {

  11. // perform alternative actions

  12. }


  13. }


  14. }

如果希望当前锁的模块不是立刻执行,也可以调用 await 机制

 
   
   
 
  1. Lock lock = new ReentrantLock();

  2. lock.lock();

  3. try {

  4. // manipulate protected state

  5. lock.newCondition().await(5, TimeUnit.SECONDS);

  6. }

  7. finally {

  8. lock.unlock();

  9. }

有时候,当遇到过长的业务流程,导致持有的时间太长了,可以考虑打断锁的机制,释放锁。

 
   
   
 
  1. Lock lock = new ReentrantLock();

  2. lock.lock();

  3. try {

  4. // manipulate protected state

  5. long startTime = System.currentTimeMillis();

  6. long endTime = System.currentTimeMillis();

  7. if (endTime - startTime > 10) {

  8. lock.lockInterruptibly();

  9. try {

  10. } finally {

  11. lock.unlock();

  12. }

  13. }


  14. lock.newCondition().await(5, TimeUnit.SECONDS);

  15. }

  16. finally {

  17. lock.unlock();

  18. }

应用场景比较

方法 说明 适合场景 注意事项
lock 获取锁 保证顺序,同步进行 必须小心以确保通过try-finally或try-catch保护持有锁定时执行的所有代码,以确保在必要时释放锁定
tryLock 仅在调用时释放锁时才获取锁 在没有锁的情况下,可以去做别的事情,充分利用线程 如果加上了获取时间,必须记录情况和异常类型
lockInterruptibly 除非当前线程被中断,否则获取锁 业务时间过长,持有的锁太久,可以直接中断 必须记录情况和异常类型
newCondition 绑定条件在锁上 实现一个延迟加锁机制 必须记录情况
unlock 释放锁 只要使用到了锁,最终都要必须释放锁 只要使用上了锁,必须使用到该方法

源码

先看类

 
   
   
 
  1. public class ReentrantLock implements Lock, java.io.Serializable {

  2. private static final long serialVersionUID = 7373984872572414699L;

向上继承了 Lock 接口,以及 Serializable,都是实现了 Lock 的方法。

  • void lock() 获取锁

  • void lockInterruptibly() 中断锁机制

  • boolean tryLock() 其他线程有释放锁,才调用获取锁

  • boolean tryLock(long time, TimeUnit unit) 给定时间内线程是空闲时间,且其他线程有释放锁,才调用获取锁

  • void unlock() 释放锁

  • Condition newCondition() 给锁绑定条件

实例化锁

 
   
   
 
  1. Lock lock = new ReentrantLock();


  2. Lock lock1 = new ReentrantLock(false);


  3. Lock lock2 = new ReentrantLock(true);

这里,有两个构造,一个是无参构造,一个是传入布尔值。

 
   
   
 
  1. /**

  2. * Creates an instance of {@code ReentrantLock}.

  3. * This is equivalent to using {@code ReentrantLock(false)}.

  4. */

  5. public ReentrantLock() {

  6. // 创建一个非公平锁

  7. sync = new NonfairSync();

  8. }


  9. /**

  10. * Creates an instance of {@code ReentrantLock} with the

  11. * given fairness policy.

  12. *

  13. * @param fair {@code true} if this lock should use a fair ordering policy

  14. */

  15. public ReentrantLock(boolean fair) {

  16. // true 公平锁 false 非公平锁

  17. sync = fair ? new FairSync() : new NonfairSync();

  18. }

这里默认是构造一个非公平锁,也可以直接设置创建什么类型锁,比如公平锁、非公平锁。

我们先看看公平锁。

 
   
   
 
  1. /**

  2. * Sync object for fair locks

  3. */

  4. static final class FairSync extends Sync {

  5. private static final long serialVersionUID = -3000897897090466540L;


  6. final void lock() {

  7. // 当使用 lock.lock() 的时候调到 同时传入1

  8. acquire(1);

  9. }


  10. /**

  11. * Fair version of tryAcquire. Don't grant access unless

  12. * recursive call or no waiters or is first.

  13. */

  14. // 这里 acquires = 1指的是 上锁的状态 0则是未上锁

  15. protected final boolean tryAcquire(int acquires) {

  16. // 获取当前线程对象

  17. final Thread current = Thread.currentThread();

  18. // 是否上锁,首次默认是0

  19. int c = getState();

  20. if (c == 0) {

  21. // hasQueuedPredecessors 是否是当前线程,使用了双向链表的数据结构,做一个 queue,这里的意思大概是到了这个线程可以加锁了。不用排队了

  22. if (!hasQueuedPredecessors() &&

  23. // CAS 比较后修改成功

  24. compareAndSetState(0, acquires)) {

  25. // 将当前线程设置成独占线程

  26. setExclusiveOwnerThread(current);

  27. // 获取锁成功

  28. return true;

  29. }

  30. }

  31. // 独占线程

  32. else if (current == getExclusiveOwnerThread()) {

  33. // nextc = 1

  34. int nextc = c + acquires;

  35. if (nextc < 0)

  36. throw new Error("Maximum lock count exceeded");

  37. // 同步锁的状态 = 1

  38. setState(nextc);

  39. return true;

  40. }

  41. // 其他线程则未获取到锁

  42. return false;

  43. }

  44. }


  45. /**

  46. * Acquires in exclusive mode, ignoring interrupts. Implemented

  47. * by invoking at least once {@link #tryAcquire},

  48. * returning on success. Otherwise the thread is queued, possibly

  49. * repeatedly blocking and unblocking, invoking {@link

  50. * #tryAcquire} until success. This method can be used

  51. * to implement method {@link Lock#lock}.

  52. *

  53. * @param arg the acquire argument. This value is conveyed to

  54. * {@link #tryAcquire} but is otherwise uninterpreted and

  55. * can represent anything you like.

  56. */

  57. public final void acquire(int arg) {

  58. // 尝试上锁

  59. if (!tryAcquire(arg) &&

  60. // 先将线程当前线程添加进队列最后一个,然后在判断是否到他上锁了

  61. acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

  62. // 没有则直接打断线程

  63. selfInterrupt();

  64. }



  65. /**

  66. * Acquires in exclusive uninterruptible mode for thread already in

  67. * queue. Used by condition wait methods as well as acquire.

  68. *

  69. * @param node the node

  70. * @param arg the acquire argument

  71. * @return {@code true} if interrupted while waiting

  72. */

  73. // 持有锁的队列里面的线程

  74. final boolean acquireQueued(final Node node, int arg) {

  75. boolean failed = true;

  76. try {

  77. boolean interrupted = false;

  78. // 一个死循环你判断里面当前谁获得锁

  79. for (;;) {

  80. // 指向下一个节点

  81. final Node p = node.predecessor();

  82. // 队列的第一个,且获取锁,将当前线程变成独占线程

  83. if (p == head && tryAcquire(arg)) {

  84. setHead(node);

  85. p.next = null; // help GC

  86. failed = false;

  87. // 如果获得到锁,则不需要打断当前线程返回一个 false

  88. return interrupted;

  89. }

  90. // 线程阻塞了

  91. if (shouldParkAfterFailedAcquire(p, node) &&

  92. parkAndCheckInterrupt())

  93. // 中断获取锁的机制

  94. interrupted = true;

  95. }

  96. } finally {

  97. // 如果获得到锁 则不需要取消加锁操作、反之则取消

  98. if (failed)

  99. cancelAcquire(node);

  100. }

  101. }

公平锁,当前的线程会进入一个队列中最后一个,等待到他的上锁。如果当前的线程表示获取到锁,且也是队列的第一个位置。会将自己变成锁的独占线程,只有自己才持有这个锁的对象。同时这里进行了 CAS 的原子交换,设置状态为1。其中 0 为 未上锁状态,1为上锁状态。

非公平锁

 
   
   
 
  1. /**

  2. * Sync object for non-fair locks

  3. */

  4. static final class NonfairSync extends Sync {

  5. private static final long serialVersionUID = 7316153563782823691L;


  6. /**

  7. * Performs lock. Try immediate barge, backing up to normal

  8. * acquire on failure.

  9. */

  10. final void lock() {

  11. // 先进行抢占锁,如果如可以修改状态,则直接变成上锁状态

  12. if (compareAndSetState(0, 1))

  13. // 设置独占线程

  14. setExclusiveOwnerThread(Thread.currentThread());

  15. else

  16. // 加锁

  17. acquire(1);

  18. }


  19. protected final boolean tryAcquire(int acquires) {

  20. return nonfairTryAcquire(acquires);

  21. }

  22. }


  23. /**

  24. * Performs non-fair tryLock. tryAcquire is implemented in

  25. * subclasses, but both need nonfair try for trylock method.

  26. */

  27. final boolean nonfairTryAcquire(int acquires) {

  28. // 这个和公平锁里面差不多

  29. final Thread current = Thread.currentThread();

  30. int c = getState();

  31. if (c == 0) {

  32. if (compareAndSetState(0, acquires)) {

  33. setExclusiveOwnerThread(current);

  34. return true;

  35. }

  36. }

  37. else if (current == getExclusiveOwnerThread()) {

  38. int nextc = c + acquires;

  39. if (nextc < 0) // overflow

  40. throw new Error("Maximum lock count exceeded");

  41. setState(nextc);

  42. return true;

  43. }

  44. return false;

  45. }

非公平锁,先进行抢占锁,先查看线程是否释放了锁,如果释放了,当前线程则直接上锁,这样的好处就是减少了线程之间的等待,加快了上锁的机制,避免了排队时竞争所导致的延时,提高了性能,如果没有释放锁,则进入到队列的最后一个等待上锁。

小结一下。

锁的类型 机制 效率
公平锁 将线程放进队列的最后一个,等待上锁 一般
非公平 先进行抢占锁,获取到锁则直接上锁,没有则将线程放进队列的最后一个等待上锁 较高

释放锁机制。

 
   
   
 
  1. /**

  2. * Attempts to release this lock.

  3. *

  4. * <p>If the current thread is the holder of this lock then the hold

  5. * count is decremented. If the hold count is now zero then the lock

  6. * is released. If the current thread is not the holder of this

  7. * lock then {@link IllegalMonitorStateException} is thrown.

  8. *

  9. * @throws IllegalMonitorStateException if the current thread does not

  10. * hold this lock

  11. */

  12. public void unlock() {

  13. // 携带 1

  14. sync.release(1);

  15. }



  16. public final boolean release(int arg) {

  17. // 释放成功则为 true 否则是 false

  18. if (tryRelease(arg)) {

  19. Node h = head;

  20. if (h != null && h.waitStatus != 0)

  21. // 唤醒队列后面的线程

  22. unparkSuccessor(h);

  23. // 释放成功

  24. return true;

  25. }

  26. // 释放失败

  27. return false;

  28. }




  29. protected final boolean tryRelease(int releases) {


  30. // c = 0

  31. int c = getState() - releases;

  32. // 如果不是当前线程和不是独占线程

  33. if (Thread.currentThread() != getExclusiveOwnerThread())

  34. // 直接抛出错

  35. throw new IllegalMonitorStateException();

  36. boolean free = false;

  37. if (c == 0) {

  38. // 释放锁

  39. free = true;

  40. // 独占线程设置null

  41. setExclusiveOwnerThread(null);

  42. }

  43. // 0

  44. setState(c);

  45. return free;

  46. }

锁的释放,比较简单。将锁状态重新设置回 0,同时独占线程也设置null,之后唤醒后面的队列里面的线程,完成释放。

源码总结

ReentrantLock 创建的时候,默认是非公平锁,不过你也可以在构造的时候,也可以创建一个公平锁。 其中通过 CAS 改变 state 的状态来改变锁的数值, 0 表示有锁可以获取,1 表示锁已被获取,来设置锁的独占线程。

在公平锁的机制中,请求锁的线程会直接排到一个队列中(通过一个双向链表来模拟的队列)的最后一个,去获取锁。

非公平锁的机制中,请求锁的线程首先会先通过 CAS 来改变 state 的锁状态,如果可以改变(0 -> 1),则直接获取到锁,将自身设置成独占锁。这样的好处就减少了一些进队列、加载队列、唤醒线程等性能消耗。如果未能修改到 state 的状态,也会变成公平锁的机制,进入到队列的最后一个,等待到它去获取锁。

锁的释放,将 state 重新设置回 0,同时独占线程(你也可以认为这是持有锁的线程对象)设置null,之后唤醒排在它下个的线程。这一系列步骤做完,则宣告锁的释放。

优缺点

非公平锁,确实性能比较高。不过也有一个显而易见的缺点,我们可以想象一下,当你在排队吃饭的时候,轮到你吃饭的时候,这时候突然来一个人插在你前面,提前打饭了,导致你打饭时间变长了,如果这时候在有几个人在也突然插到你前打饭,又会继续导致你打饭时间变得更长。那如果放到线程里面,突然其他线程提前获取到了锁,那会导致当前线程获取到锁时间变长,而导致线程阻塞,迟迟未获取到锁。

所以得根据业务去选择合适的锁类型,进行上锁,尽可能的避免有一些重要的业务因为上锁而阻塞到。

引用

  • [1] Lock (Java Platform SE 8 )

以上是关于jdk 源码系列之ReentrantLock的主要内容,如果未能解决你的问题,请参考以下文章

JDK源码之ReentrantLock源码分析

JDK源码之ReentrantLock源码分析

JDK类库源码分析系列2-AbstractQueuedSynchronizer-ReentrantReadWriteLock

源码分析:①ReentrantLock之公平锁和非公平锁

ReentrantLock源码分析--jdk1.8

ReentrantLock源码分析-JDK1.8