AQS源码解读(三)——ReentrantLock原理详解(Sync、NonfairSync、FairSync)

偏执的太偏执、 2022-12-12 04:44 277阅读 0赞

天青色等烟雨,而我在等你,微信公众号搜索:徐同学呀,持续更新肝货,快来关注我,和我一起学习吧~

更多JUC源码解读系列文章请持续关注JUC源码解读文章目录JDK8 !


文章目录

    • 一、前言
    • 二、ReentrantLock基本结构
    • 三、锁的公平性
    • 四、锁的获取与释放
      • 1、ReentrantLock#lock
        • (1)ReentrantLock.NonfairSync
        • (2)ReentrantLock.FairSync
      • 2、ReentrantLock#lockInterruptibly
      • 3、ReentrantLock#tryLock
      • 4、ReentrantLock#unlock
    • 五、总结
    • 文末有彩蛋

一、前言

ASQ实现的是一套通用的模板,并不能完全直接应用于实际并发生产中,ReentrantLock就是根据AQS实现的互斥可重入锁。ReentrantLocksynchronized类似,互斥、阻塞、可重入,不同之处在于synchronized基于Java语法层面实现隐式加锁和释放锁,ReentrantLock基于API层面实现显式加锁和释放锁。

ReentrantLock实现了接口Lock,相较于synchronized,对锁的操作更灵活可控,同时根据不同的环境实现了公平锁和非公平锁。


二、ReentrantLock基本结构

ReentrantLock本身没有什么代码逻辑,实现的方法调用的是Sync的模板方法,所以锁机制的所有实现逻辑都在ReentrantLock的内部类Sync中。Sync继承自AbstractQueuedSynchronizer,其还有两个子类NonfairSyncFairSync,分别具体实现非公平和公平情况下加锁的逻辑。如下是ReentrantLock的类关系图:
ReentrantLock的类关系图

  1. public class ReentrantLock implements Lock, java.io.Serializable {
  2. private static final long serialVersionUID = 7373984872572414699L;
  3. /** Synchronizer providing all implementation mechanics */
  4. //sync同步器提供所有的实现机制
  5. private final Sync sync;
  6. /** * Base of synchronization control for this lock. Subclassed * into fair and nonfair versions below. Uses AQS state to * represent the number of holds on the lock. */
  7. abstract static class Sync extends AbstractQueuedSynchronizer {
  8. abstract void lock();
  9. ... ...
  10. }
  11. static final class NonfairSync extends Sync {
  12. final void lock() { }
  13. ... ...
  14. }
  15. static final class FairSync extends Sync {
  16. final void lock() { }
  17. ... ...
  18. }
  19. ... ...
  20. }

三、锁的公平性

ReentrantLock默认情况下提供非公平锁,同时也可以在实例化的时候指定锁公平性。

  1. public ReentrantLock() {
  2. sync = new NonfairSync();
  3. }
  4. public ReentrantLock(boolean fair) {
  5. sync = fair ? new FairSync() : new NonfairSync();
  6. }

何为公平和非公平,例如实际生活中的排队,新来的人按规矩排在队尾是公平,插队就是非公平。对于锁也是同样的道理,新来线程排在阻塞队列队尾是公平,一上来就抢锁是非公平。不同于现实生活,ReentrantLock默认非公平锁是为了减少线程间的切换,从而提高效率。


四、锁的获取与释放

ReentrantLock锁的获取分别实现了不可中断获取锁lock,尝试获取锁tryLock(获取成功返回true,获取失败返回false),可中断获取锁lockInterruptibly;锁的释放unlock很简单,直接调用的AQS的模板方法release,不需要区分公平性。

1、ReentrantLock#lock

ReentrantLock#lock调用了sync.lock()Synclock()是一个抽象函数,具体的实现在其子类NonfairSyncFairSync中。

  1. public void lock() {
  2. sync.lock();
  3. }
  4. abstract static class Sync extends AbstractQueuedSynchronizer {
  5. /** * Performs {@link Lock#lock}. The main reason for subclassing * is to allow fast path for nonfair version. */
  6. abstract void lock();
  7. }

(1)ReentrantLock.NonfairSync

ReentrantLock.NonfairSync#lock实现了Sync中的抽象方法lock,其非公平性体现在,一上来就抢锁设置statecompareAndSetState(0, 1),如果抢锁成功就设置当前线程为持有独占锁的线程setExclusiveOwnerThread(Thread.currentThread())

  1. static final class NonfairSync extends Sync {
  2. private static final long serialVersionUID = 7316153563782823691L;
  3. /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */
  4. final void lock() {
  5. //非公平,直接抢锁
  6. if (compareAndSetState(0, 1))
  7. //抢成功,设置当前独占锁的线程为当前线程
  8. setExclusiveOwnerThread(Thread.currentThread());
  9. else
  10. //没有抢到
  11. acquire(1);
  12. }
  13. protected final boolean tryAcquire(int acquires) {
  14. return nonfairTryAcquire(acquires);
  15. }
  16. }

如果一上来抢锁失败则进入AQS的模板方法acquire(1)acquire(1)中还会再进行一次抢锁tryAcquire,抢锁失败才进入AQS队列操作acquireQueued(addWaiter(Node.EXCLUSIVE), arg)。(acquire详解请看拙作《AQS源码解读——从acquireQueued探索独占锁实现原理,如何阻塞?如何唤醒?》)

  1. //AbstractQueuedSynchronizer#acquire
  2. public final void acquire(int arg) {
  3. //若没有抢到锁,则进入等待队列
  4. if (!tryAcquire(arg) &&
  5. acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
  6. //自己中断自己
  7. selfInterrupt();
  8. }

加锁的通用模板acquire()已经在AQS中实现,子类只需要实现tryAcquire即可。NonfairSync#tryAcquire调用了父类的Sync#nonfairTryAcquire。具体逻辑如下:

  • 查看当前state,是否有线程持有锁,没有state=0则继续抢锁。
  • 有线程持有锁,判断持有锁的线程是否是当前线程,是就重入。

    //ReentrantLock.Sync#nonfairTryAcquire
    final boolean nonfairTryAcquire(int acquires) {

    1. final Thread current = Thread.currentThread();
    2. int c = getState();
    3. if (c == 0) {
    4. //若此时锁空着,则再次尝试抢锁
    5. if (compareAndSetState(0, acquires)) {
    6. setExclusiveOwnerThread(current);
    7. return true;
    8. }
    9. }
    10. //若当前持锁线程是当前线程(重入性)
    11. else if (current == getExclusiveOwnerThread()) {
    12. int nextc = c + acquires;
    13. if (nextc < 0) // overflow
    14. throw new Error("Maximum lock count exceeded");
    15. //重入
    16. setState(nextc);
    17. return true;
    18. }
    19. return false;

    }

(2)ReentrantLock.FairSync

FairSyncNonfairSync的主要区别在于,FairSync不会一上来就抢锁,而是先判断队列中是否有其他线程在等待锁,没有再抢锁。

代码的模板逻辑和NonfairSync类似,区别在于FairSynctryAcquire的实现。

  1. static final class FairSync extends Sync {
  2. private static final long serialVersionUID = -3000897897090466540L;
  3. final void lock() {
  4. acquire(1);
  5. }
  6. /** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */
  7. protected final boolean tryAcquire(int acquires) {
  8. final Thread current = Thread.currentThread();
  9. int c = getState();
  10. if (c == 0) {
  11. //是否有线程在等待锁,没有则抢锁
  12. if (!hasQueuedPredecessors() &&
  13. compareAndSetState(0, acquires)) {
  14. setExclusiveOwnerThread(current);
  15. return true;
  16. }
  17. }
  18. //如果当前线程已经持有该锁,重入
  19. else if (current == getExclusiveOwnerThread()) {
  20. int nextc = c + acquires;
  21. if (nextc < 0)
  22. throw new Error("Maximum lock count exceeded");
  23. setState(nextc);
  24. return true;
  25. }
  26. return false;
  27. }
  28. }

FairSync的公平性主要体现在tryAcquire判断当前没有线程持有锁时,不会立即去抢锁,而是判断AQS队列中是否有其他线程也在等待锁,没有才去抢锁。如果有线程正持有锁,判断是否是当前线程,是则重入。

  1. public final boolean hasQueuedPredecessors() {
  2. // The correctness of this depends on head being initialized
  3. // before tail and on head.next being accurate if the current
  4. // thread is first in queue.
  5. Node t = tail; // Read fields in reverse initialization order
  6. Node h = head;
  7. Node s;
  8. return h != t &&
  9. ((s = h.next) == null || s.thread != Thread.currentThread());
  10. }

2、ReentrantLock#lockInterruptibly

lock是不可中断锁,lockInterruptibly是可中断锁,其直接调用了AQS的模板方法acquireInterruptibly

  1. public void lockInterruptibly() throws InterruptedException {
  2. sync.acquireInterruptibly(1);
  3. }

acquireInterruptiblydoAcquireInterruptibly代码中分别响应中断抛出异常InterruptedException

acquireInterruptiblydoAcquireInterruptibly详解请看拙作《AQS源码解读——从acquireQueued探索独占锁实现原理,如何阻塞?如何唤醒?》)

3、ReentrantLock#tryLock

tryLock尝试获取锁,调用的是sync.nonfairTryAcquire(1),不会涉及到AQS队列操作,获取锁成功返回true,失败返回false。

  1. public boolean tryLock() {
  2. return sync.nonfairTryAcquire(1);
  3. }

tryLock还有一个重载方法,可传入一个超时时长timeout和一个时间单位TimeUnit,超时时长会被转为纳秒级。

  1. public boolean tryLock(long timeout, TimeUnit unit)
  2. throws InterruptedException {
  3. return sync.tryAcquireNanos(1, unit.toNanos(timeout));
  4. }

tryLock(long timeout, TimeUnit unit)直接调用了AQS的模板方法tryAcquireNanos,也具备了响应中断,超时获取锁的功能:

  1. 若一开始获取锁tryAcquire失败则进入AQS同步队列doAcquireNanos
  2. 进入同步队列后自旋1000纳秒,还没有获取锁且判断应该阻塞,则会阻塞一定时长。
  3. 超时时长到线程自动唤醒,再自旋还没获取锁,且判断超时则返回false。

tryAcquireNanos详解请看拙作《AQS源码解读——从acquireQueued探索独占锁实现原理,如何阻塞?如何唤醒?》)

4、ReentrantLock#unlock

ReentrantLock释放锁的过程没有区分公平性,调用的是AQS的模板方法release(),其基本逻辑如下:

  1. 释放锁tryRelease
  2. 唤醒后继节点unparkSuccessor。(unparkSuccessor详解请看拙作《AQS源码解读——从acquireQueued探索独占锁实现原理,如何阻塞?如何唤醒?》)

    //ReentrantLock
    public void unlock() {

    1. sync.release(1);

    }
    //AbstractQueuedSynchronizer
    public final boolean release(int arg) {

    1. if (tryRelease(arg)) {
    2. Node h = head;
    3. if (h != null && h.waitStatus != 0)
    4. //释放锁成功后唤醒head后继节点。
    5. unparkSuccessor(h);
    6. return true;
    7. }
    8. return false;

    }

释放锁的逻辑由ReentrantLock实现,释放锁之后唤醒后继由AQS负责。释放锁的代码ReentrantLock.Sync#tryRelease基本逻辑如下:

  1. 判断持有锁的线程是否是当前线程,不是当前线程则抛出异常。
  2. 判断释放之后的state==0,等于0说明完全释放锁,将持锁的线程设置为null。
  3. 修改state,因为是排它锁,只有当前线程才会走到这里,所以是线程安全的。

注:如果是当前线程多次重入,releases=1是不能完全释放锁的,free=false,也不会唤醒后继节点。

  1. //ReentrantLock.Sync#tryRelease
  2. protected final boolean tryRelease(int releases) {
  3. //释放锁之后的state值
  4. int c = getState() - releases;
  5. if (Thread.currentThread() != getExclusiveOwnerThread())
  6. //不是当前线程,不能unLock 抛异常
  7. throw new IllegalMonitorStateException();
  8. boolean free = false;
  9. if (c == 0) {
  10. //每次减一,c = 0,证明没有线程持有锁了,可以释放了
  11. free = true;
  12. setExclusiveOwnerThread(null);
  13. }
  14. setState(c); //排它锁,只有当前线程才会走到这,是线程安全的 修改state
  15. return free;
  16. }

五、总结

  • ReentrantLock通过AQS实现了互斥锁的逻辑,核心模板代码都在AQS中,ReentrantLock中只需要实现tryAcquiretryRelease即可。
  • ReentrantLock是互斥锁且可重入。
  • ReentrantLock实现了不可中断获取锁,可中断获取锁,可超时获取锁。
  • ReentrantLock获取锁有公平与非公平之分,释放锁没有。

PS: 如若文章中有错误理解,欢迎批评指正,同时非常期待你的评论、点赞和收藏。我是徐同学,愿与你共同进步!
打开微信搜一搜


文末有彩蛋

《Java开发手册(泰山版)》中对Lock的使用做了几点强制的规范:

【强制】在使用阻塞等待获取锁的方式中,必须在 try 代码块之外,并且在加锁方法与 try 代

码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在 finally 中无法解锁。

说明一:如果在 lock 方法与 try 代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功

获取锁。

说明二:如果 lock 方法在 try 代码块之内,可能由于其它方法抛出异常,导致在 finally 代码块中,unlock

对未加锁的对象解锁,它会调用 AQS 的 tryRelease 方法(取决于具体实现类),抛出

IllegalMonitorStateException 异常。

说明三:在 Lock 对象的 lock 方法实现中可能抛出 unchecked 异常,产生的后果与说明二相同。

正例:

  1. Lock lock = new XxxLock();
  2. // ...
  3. lock.lock();
  4. try {
  5. doSomething();
  6. doOthers();
  7. } finally {
  8. lock.unlock();
  9. }

反例:

  1. Lock lock = new XxxLock();
  2. // ...
  3. try {
  4. // 如果此处抛出异常,则直接执行 finally 代码块
  5. doSomething();
  6. // 无论加锁是否成功,finally 代码块都会执行
  7. lock.lock();
  8. doOthers();
  9. } finally {
  10. lock.unlock();
  11. }

【强制】在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否

持有锁。锁的释放规则与锁的阻塞等待方式相同。

说明:Lock 对象的 unlock 方法在执行时,它会调用 AQS 的 tryRelease 方法(取决于具体实现类),如果

当前线程不持有锁,则抛出 IllegalMonitorStateException 异常。

正例:

  1. Lock lock = new XxxLock();
  2. // ...
  3. boolean isLocked = lock.tryLock();
  4. if (isLocked) {
  5. try {
  6. doSomething();
  7. doOthers();
  8. } finally {
  9. lock.unlock();
  10. }
  11. }

发表评论

表情:
评论列表 (有 0 条评论,277人围观)

还没有评论,来说两句吧...

相关阅读