• 多线程JUC 第2季 ReentranctLock实现加锁和解锁过程


    一 ReentranctLock

    1.1 ReentranctLock

    1.ReentrantLock 意思为可重入锁,指的是一个线程能够对一个临界资源重复加锁。ReentrantLock内部实现依赖于AQS。

    1.2 ReentranctLock的结构组成

    ReentrantLock有三个内部类:
    Sync:继承自AQS的同步控制基础。
    NonfairSync:Sync的非公平版本实现
    FairSync:Sync的公平版本实现
    ReentrantLock默认实例化的非公平锁NonfairSync。可以通过传递true,来实例化公平锁FairSync

    1.3 ReentranctLock的加锁和解锁流程

    1)FairSync加锁过程:
    1.调用lock()方法即可加锁。
    2.加锁时先tryAcquire(1)尝试获取锁:
    2.1 如果获取成功,那么把当前线程设置为工作线程,结束加锁流程
    2.2 如果获取失败,那么把当前线程添加到CLH队列的队尾进行等待。
    2)FairSync解锁过程:
    1.调用unlock()方法即可解锁。注意,该方法一定要放在finally块中进行调用!
    2.解锁时调用tryRelease(arg)方法
    2.1如果成功,唤醒等待队列中的线程进行工作
    2.2如果失败,返回false

    3)NonFairSync加锁解锁过程:
    1.NonFairSync会在调用lock()时,直接尝试把state设置为1:
    1.1如果失败,会调用nonfairTryAcquire()获取锁,而nonfairTryAcquire()在获取锁的时候,也不会顾及CLH队列中是否有线程在等待获取锁,从而实现非公平加锁。
    1.2如果成功,直接获取锁,执行业务。
    4)解锁过程和FairSync一致。

    二 ReentranctLock源码分析

    2.1 公平锁

    2.1.1 lock方法

    1.看到lock()方法其实就是调用Sync的lock()方法,而Sync的Lock有NonFairSync和FairSync两种实现。

    2.这边先看FairSync的实现,该方法的参数1,实际上即是state的值,1表示加锁。继续看acquire()方法:

    3.acquire()调用了三个方法,分别是:

       tryAcquire():尝试以独占模式获取锁

       addWaiter():为当前线程和给定模式创建并排队节点。

        acquireQueued():以独占不中断模式获取已在队列中的线程

    如下图所示:

     3.1 tryAcquire()方法

    1. protected final boolean tryAcquire(int acquires) {
    2. // 获取当前线程
    3. final Thread current = Thread.currentThread();
    4. // 获取state的值
    5. int c = getState();
    6. // state等于0,即共享资源没有被加锁
    7. if (c == 0) {
    8. /*
    9. 先看队列中有没有线程在等待获取锁,
    10. 如果没有那就设置当前线程就是工作线程
    11. 如果有就返回false,此处体现了公平锁特性
    12. */
    13. if (!hasQueuedPredecessors() &&
    14. compareAndSetState(0, acquires)) {
    15. // 设置当前线程为独占线程
    16. setExclusiveOwnerThread(current);
    17. return true;
    18. }
    19. }
    20. // 如果当前工作线程是该线程,那么可以重复获取锁,并将state的值+1,此处体现了ReentrantLock是可重入锁
    21. else if (current == getExclusiveOwnerThread()) {
    22. int nextc = c + acquires;
    23. // int有最大值,超过最大值后,state会变成负数,因此加了这个判断
    24. if (nextc < 0)
    25. throw new Error("Maximum lock count exceeded");
    26. setState(nextc);
    27. return true;
    28. }
    29. // 共享资源被锁定,返回false
    30. return false;
    31. }

    这个方法的第一个if判断,提现了获取锁是公平的;第二个则体现是ReentrantLock是可重入锁。

    看一下hasQueuedPredecessors()方法,

    hasQueuedPredecessors():代码逻辑如下:

    1. public final boolean hasQueuedPredecessors() {
    2. Node t = tail;
    3. Node h = head;
    4. Node s;
    5. // 头结点不等于尾节点,并且,(头结点的next不为null,或者next的线程不是当前线程)
    6. return h != t &&
    7. ((s = h.next) == null || s.thread != Thread.currentThread());
    8. }

    3.2 addWaiter():

    1. private Node addWaiter(Node mode) {
    2. // 把当前线程作为入参创建一个Node节点
    3. Node node = new Node(Thread.currentThread(), mode);
    4. // Try the fast path of enq; backup to full enq on failure
    5. Node pred = tail;
    6. // 如果队列的尾节点不为null,把node添加到队尾
    7. if (pred != null) {
    8. node.prev = pred;
    9. // cas设置尾节点
    10. if (compareAndSetTail(pred, node)) {
    11. pred.next = node;
    12. return node;
    13. }
    14. }
    15. // 将节点插入队列,如果队列为空则初始化队列
    16. enq(node);
    17. return node;
    18. }

    addWaiter(),就是把当前线程添加到队列中。 其中的enq()是在快速添加node到队尾失败才会调用,而这个方法也是添加node到队列中。

    acquireQueued():

    1. final boolean acquireQueued(final Node node, int arg) {
    2. // 标记是否成功拿到资源
    3. boolean failed = true;
    4. try {
    5. // 标记等待过程中是否中断过
    6. boolean interrupted = false;
    7. // 开始自旋,要么获取锁,要么中断
    8. for (;;) {
    9. // 获取当前节点的前节点
    10. final Node p = node.predecessor();
    11. // 如果前节点是头结点(工作节点)并且tryAcquire()成功
    12. if (p == head && tryAcquire(arg)) {
    13. // 把node设置为头结点
    14. setHead(node);
    15. p.next = null; // help GC
    16. failed = false;
    17. return interrupted;
    18. }
    19. // 靠前驱节点判断当前线程是否应该被阻塞
    20. if (shouldParkAfterFailedAcquire(p, node) &&
    21. // 阻塞当前节点
    22. parkAndCheckInterrupt())
    23. interrupted = true;
    24. }
    25. } finally {
    26. // 如果执行失败,则把这个节点状态设置为取消状态
    27. if (failed)
    28. cancelAcquire(node);
    29. }
    30. }

    这边多了shouldParkAfterFailedAcquire()和parkAndCheckInterrupt()方法。

    shouldParkAfterFailedAcquire():靠前驱节点判断当前线程是否应该被阻塞

    1. // 靠前驱节点判断当前线程是否应该被阻塞
    2. private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    3. // 获取头结点的节点状态
    4. int ws = pred.waitStatus;
    5. // 说明头结点处于唤醒状态
    6. if (ws == Node.SIGNAL)
    7. return true;
    8. // 通过枚举值我们知道waitStatus>0是取消状态
    9. if (ws > 0) {
    10. do {
    11. // 循环向前查找取消节点,把取消节点从队列中剔除
    12. node.prev = pred = pred.prev;
    13. } while (pred.waitStatus > 0);
    14. pred.next = node;
    15. } else {
    16. // 设置前任节点等待状态为SIGNAL
    17. compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    18. }
    19. return false;
    20. }

    parkAndCheckInterrupt():阻塞当前节点

    1. private final boolean parkAndCheckInterrupt() {
    2. // 阻塞当前线程
    3. LockSupport.park(this);
    4. // 当前线程被唤醒后,返回线程的中断状态,然后重置中断状态
    5. return Thread.interrupted();
    6. }

    2.1.2 unlock方法

    1.在调用lock.unlock()时候,内部调用的是Sync的release()方法,源码:

    2.release(): 

    1. public final boolean release(int arg) {
    2. // 释放锁
    3. if (tryRelease(arg)) {
    4. Node h = head;
    5. // 如果当前头结点不为null,并且waitStatus不是0(无锁状态)
    6. if (h != null && h.waitStatus != 0)
    7. // 唤醒下一个线程开展业务
    8. unparkSuccessor(h);
    9. return true;
    10. }
    11. return false;
    12. }

    release()会调用tryRelease()方法:

    • 如果返回tryRelease()false,那么release()直接返回false
    • 如果返回true,进行判断后,会唤醒队列中下一个线程进行工作

    3.tryRelease():

    1. protected final boolean tryRelease(int releases) {
    2. // 获取state的值,并且减去1(releases等于1
    3. int c = getState() - releases;
    4. // 判断当前线程是不是工作线程,如果不是,抛出异常
    5. if (Thread.currentThread() != getExclusiveOwnerThread())
    6. throw new IllegalMonitorStateException();
    7. // 共享资源是否空闲的标记
    8. boolean free = false;
    9. // 如果state为0(空闲)
    10. if (c == 0) {
    11. // 设置空闲标记为true,并把当前工作线程设置为null
    12. free = true;
    13. setExclusiveOwnerThread(null);
    14. }
    15. // 设置state等于0
    16. setState(c);
    17. return free;
    18. }

    tryRelease()会释放一次锁,不过ReentrantLock是可重入锁,因此释放一次后,共享资源不一定会处于空闲状态。

    4.unparkSuccessor():

    1. private void unparkSuccessor(Node node) {
    2. // 获取头结点waitStatus
    3. int ws = node.waitStatus;
    4. if (ws < 0)
    5. compareAndSetWaitStatus(node, ws, 0);
    6. // 获取当前节点的下一个节点
    7. Node s = node.next;
    8. // 如果下个节点是null或者下个节点被cancelled,就找到队列最开始的非cancelled的节点
    9. if (s == null || s.waitStatus > 0) {
    10. s = null;
    11. // 就从尾部节点开始找,到队首,找到队列第一个waitStatus<0的节点。
    12. for (Node t = tail; t != null && t != node; t = t.prev)
    13. if (t.waitStatus <= 0)
    14. s = t;
    15. }
    16. // 如果当前节点的下个节点不为空,而且状态<=0,就把当前节点unpark
    17. if (s != null)
    18. LockSupport.unpark(s.thread);
    19. }

    unparkSuccessor()就是唤醒线程进行工作。只有当唤醒下一个线程后,release()方法才会返回true。

    2.2 非公平锁

    2.2.1 非公平锁

    对比FairSync的源码,会发现两者的区别就是在于lock方法:

    1.非公平锁: 不公平锁加锁时,会直接尝试设置state的值。

    1. final void lock() {
    2. // 第一步直接尝试设置state的值(不公平啊)
    3. if (compareAndSetState(0, 1))
    4. // 设置成功,设置当前线程为工作线程
    5. setExclusiveOwnerThread(Thread.currentThread());
    6. else
    7. // 设置失败,再去尝试获取锁
    8. acquire(1);
    9. }

     2.在看accuire方法:可以看到和FairSync的acquire()方法一样,唯一不同的是tryAcquire()的不同;

    1. public final void acquire(int arg) {
    2. if (!tryAcquire(arg) &&
    3. acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    4. selfInterrupt();
    5. }

    3.tryAcquire():tryAcquire()调用的是nonfairTryAcquire(),

    4.nonfairTryAcquire():

    对比FairSync的tryAcquire()方法和nonfair的TryAcquire()方法,可以看出唯一的区别就是FairSync的tryAcquire()方法多了一个hasQueuedPredecessors()的判断,这个判断是判断CLH队列中有没有线程在等待获取资源。而非公平锁就不管等待队列是否有等待线程,直接去尝试获取资源(不公平)。ReentrantLock加锁和解锁-CSDN博客

    1. final boolean nonfairTryAcquire(int acquires) {
    2. // 获取当前线程
    3. final Thread current = Thread.currentThread();
    4. int c = getState();
    5. // 如果共享资源处于空闲状态
    6. if (c == 0) {
    7. // 尝试设置直接设置state(不管队列中是否有排队的线程直接就插队,不公平啊)
    8. if (compareAndSetState(0, acquires)) {
    9. // 设置state成功,就把当前线程设置为工作线程
    10. setExclusiveOwnerThread(current);
    11. return true;
    12. }
    13. }
    14. // 如果当前线程就是工作线程,那么可以重复加锁
    15. else if (current == getExclusiveOwnerThread()) {
    16. int nextc = c + acquires;
    17. if (nextc < 0) // overflow
    18. throw new Error("Maximum lock count exceeded");
    19. setState(nextc);
    20. return true;
    21. }
    22. return false;
    23. }

  • 相关阅读:
    华为云 sfs 服务浅谈
    laravel-实践
    编程的终结;展望2023年AI系统方向;AI的下一个阶段
    升级pip 升级pip3的快速方法
    H3C基本配置
    vue2.0 监听用户无操作页面停留时长然后弹窗提示
    【LeetCode-困难题】239. 滑动窗口最大值
    SAP FI 系列 (033) - 应收票据的接收和承兑
    第5章 MyBatis的注解开发
    指针详解第三部分
  • 原文地址:https://blog.csdn.net/u011066470/article/details/132937457