JUC(一)-ReentrantLock源码分析

JUC 深入 ReentrantLock

一、ReentrantLock 和 synchronized 的区别

1. 核心区别

  1. ReentrantLock是个类 , synchronized 是Java提供的关键字 , 都是在JVM层面实现的互斥锁
  2. ReentrantLock 必须手动释放锁 , synchronized 是可以自动释放的

2. 效率区别

  1. 如果竞争比较激烈,推荐使用 ReentrantLock , 因为它不存在锁升级的概念。
    而synchronized存在锁升级概念,如果升级到重量级锁,
    synchronized是不存在锁降级的

3. 底层实现区别

  1. 实现原理不一样 , ReentrantLock 就是基于AQS实现的
    synchronized 是根据不同的锁级别实现的

4. 锁的功能上的区别

  1. ReentrantLock 比 synchronized 有更全面的功能
    1. ReentrantLock 可以实现 公平锁和非公平锁 , synchronized 只能是非公平锁
    2. ReentrantLock 可以设置 等待锁资源的时间 , synchronized 只能等待CPU调度

5. 选择哪个

  1. 如果对并发编程特别熟练,推荐使用 ReentrantLock , 因为其功能更丰富
    如果掌握一般般 , 使用 synchronized 更好点

二、AQS 概述

1. AQS是什么 , 字面意思理解AQS , 稍微看点源码

  1. AQS --> (AbstractQueuedSynchronizer.java 类) , AQS是JUC包下的一个基类 , JUC包下的很多功能都是基于AQS来实现的 , 例如现在讲的 ReentrantLock
    1. 阻塞队列 , 线程池 , CountDownLatch , Semaphore 等工具类 都是基于AQS来实现的
  2. Synchronizer ==> 安全的:
    1. AQS 中提供了一个 由 volatile修饰 , 并且采用CAS方式修改的 int 类型的 state 变量
  3. Queued ==> 队列
    1. AQS 中维护了一个 双向链表 , 有 head、tail 节点 , 每个节点都是Node对象
    2. Node 对象里 可以设置 锁的模式 共享的、互斥的 , 还有Node的状态
    3. 每个有效的Node对象 都需维护了一个 Thread 线程对象
  4. AQS 内部结构 和属性
    Node 的核心属性
static final class Node {
    // Node 的模式 , ReentrantLock为互斥锁为 EXCLUSIVE
    // 而读写锁中的读锁 是共享锁 , 其Node模式为 SHARED
    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;
    /* 
        Node 的状态标志 , 除了下边的四种状态其实还有个初始状态为0
     */
    
    static final int CANCELLED = 1;
    static final int SIGNAL = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;
    // Node 的状态 , 默认初始值为 0
    volatile int waitStatus;
    
    // 当前Node的前驱节点
    volatile Node prev;
    // 当前Node的后继节点
    volatile Node next;
    // 当前Node维护的Thread对象
    volatile Thread thread;
}

image.png

三、加锁流程源码剖析

3.1 加锁流程概述

3.1.1 线程拿到锁资源的判断
  1. 哪个线程通过CAS将 ReentrantLock 的核心属性 state 从 0 ==> 1 , 就表示哪个线程拿到了锁资源
  2. 这个是非公平锁的流程
    1. 获取不到锁资源进入排队时 , 如果自己需要挂起 则需要通知前驱节点 将其 waitStatus ==> -1
      image.png

3.2 三种加锁的分析 (三个方法)

3.2.1 lock 方法
1. 执行 lock 方法后 , 非公平锁和公平锁的执行流程
// 非公平锁
final void lock() {
    /**
        公平锁 - 调用 lock 方法后 不管当前锁有没有被线程持有 , 先直接基于CAS的方式 将锁的状态 state 从 0==>1
     */
    if (compareAndSetState(0, 1))
        // 表示获取锁资源 成功
        // 获取锁资源成功 , 会将当前线程设置到 exclusiveOwnerThread属性 , 代表是当前线程持有着锁资源
        setExclusiveOwnerThread(Thread.currentThread());
    else
        // 获取锁资源失败 , 尝试获取锁资源
        acquire(1);
}
// =============================================
// 公平锁
final void lock() {
    // 直接 执行 acquire 尝试获取锁资源
    acquire(1);
}
2. 不管公平还是非公平,最后都是走的 acquire方法,这里分析 acquire方法
1. acquire() 方法
public final void acquire(int arg) {
    /**
        1.  tryAcquire : 再次查看 , 当前线程是否可以尝试获取锁资源
                         !tryAcquire 说明依然没有拿到锁资源 , 如果truAcquire成功了-说明拿锁成功 则直接结束acquire方法
                         反之 - 再次尝试 还没有拿到锁资源 , 就要考虑将当前线程入队列等待了
        2. addWaiter : 将当前线程封装为Node节点 , 并插入到AQS双向链表的结尾
        3. acquireQueued : 查看当前节点有没有资格抢占锁资源(是否是队列中的第一个节点) , 如果有抢锁资格 那么就尝试获取锁资源
                           如果尝试了没抢到 , 或者根本没有抢占资格 则就尝试将当前节点 放入AQS队列中,并试图挂起线程
     */
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
        // 如果没有拿到锁资源 , 并且当前节点不是第一个排队的节点 , 那么就中断当前节点维护的线程
        selfInterrupt();
    }
    // 不进 if 说明拿到了锁资源 , 直接结束lock流程
}
2. tryAcquire : 尝试获取锁资源 , 具体实现是由公平锁和非公平锁来实现不同的逻辑
  • 非公平锁实现逻辑
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
// 非公平锁实现
final boolean nonfairTryAcquire(int acquires) {
    // 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取到当前的 state 属性
    int c = getState();
    /**
        判断 state 属性 如果为 0 则恰好证明 -> 在进这个方法之前拿到锁的其他线程 恰好也已经释放了锁资源 
        那么就尝试将当前线程 设置为持有锁资源的线程
     */
    if (c == 0) {
        // 尝试获取锁资源
        if (compareAndSetState(0, acquires)) {
            // 设置当前线程为 拿去当前锁的线程
            setExclusiveOwnerThread(current);
            // 获取锁资源成功 , 成功返回
            return true;
        }
    }
    /**
        如果锁资源还未释放 , 则判断 当前锁资源的持有线程是否为当前线程
        如果是当前线程 , 则表示 锁重入操作
     */
    else if (current == getExclusiveOwnerThread()) {
        // 改变 state , 其实也即 改变当前锁重入次数
        int nextc = c + acquires;
        /**
            如果增加了锁重入次数后 , nextc < 0 则说明 当前重入次数已经超过了最大值 , 则抛出错误
            01111111 11111111 11111111 11111111 ==> Integer.MAX_VALUE
            10000000 00000000 00000000 00000000 ==> -2147483648
         */
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        // 没有发生错误 , 则设置当前的 state 为重入计算后的 state
        setState(nextc);
        // 重入成功 返回 true
        return true;
    }
    // 当前锁没有处于释放状态 , 并且也不属于锁重入 则直接返回false
    return false;
}
  • 公平锁实现逻辑
// 公平锁实现
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        /**
         * 1. hasQueuedPredecessors 查看AQS中队列有没有线程在排队
         *    !hasQueuedPredecessors 为 true 则表示 [没有线程排队或者排队的第一个是当前线程]
         *    那么就可以去尝试获取锁资源了
         */
        if (!hasQueuedPredecessors() &&
            // 尝试获取锁资源
            compareAndSetState(0, acquires)) 
        {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 剩下的和非公平锁的 锁重入一样
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
  • 非公平锁的 hasQueuedPredecessors 方法
/** 
    查看是否有线程在 AQS双向队列中排队
    返回 false 说明没有线程排队 , 或者排队的第一个线程是当前线程
    返回 true  说明有线程在排队
*/
public final boolean hasQueuedPredecessors() {
    // 获取头尾节点
    Node t = tail;
    Node h = head;
    // 头节点的下一个节点
    Node s;
    /**
        1. 如果 头和尾是同一个对象 (h == t)  , 则说明 当前没有线程在排队
           所以 (h != t) 说明 当前队列中有排队的线程
     */
    return (h != t) && 
        /**
            2. 到下边的逻辑 则说明 当前队列有线程在排队等待
               这一串的逻辑表示 , 有线程在排队 , 那么就看排在第一名的是不是我
               如果第一名是我 则间接的表示现在AQS中没有线程在排队了 返回 false
               如果第一名不是我 则表示 AQS 中有线程在排队则       返回 true
         */
        ((s = h.next) == null || s.thread != Thread.currentThread());
}
  • tryAcquire的结论:
    1. 非公平锁 不管当前锁资源有没有被占有 都会去抢一下
    2. 公平锁 , 如果当前队列中没有线程在排队、或者排队的第一个节点是自己 才会去抢
3. addWaiter : 将没有拿到锁资源的线程 放到 AQS 双向队列中 (没有公平和非公平一说了 , 都要走acquire方法的这个方法)
  • addWaiter 方法
// 没有拿到锁资源 过来排队 , mode -> 表示当前锁类型 互斥锁、共享锁
private Node addWaiter(Node mode) {
    // 将当前线程封装为 Node 对象
    Node node = new Node(Thread.currentThread(), mode);
    // 拿到当前 AQS 中的尾部节点
    Node pred = tail;
    /**
     * 1. pred != null 说明当前 AQS 双向队列已经 初始化过了
     *    AQS 在第一次生成时 head 和 tail 都是指向 null 的节点
     *    
     */
    if (pred != null) {
        // 当前节点的 上一个节点 指向 现在AQS的尾节点 (即 链表的插入元素时做的操作)
        node.prev = pred;
        /**
         * 通过 CAS 替换当前AQS的尾节点为 当前新增的 node 节点
         * 这里 CAS 失败了也没关系 后边会走 enq方法
         */
        if (compareAndSetTail(pred, node)) {
            // 然后再把原来的尾节点的下一个节点指向 本次新增的节点
            pred.next = node;
            return node;
        }
    }
    /**
     * 2. enq 方法 确保当前AQS队列 head、tail 节点被正确初始化
     *    如果已经初始化 ,即走上边if语句 那么这个方法的作用就是 死循环保证替换尾节点的CAS操作成功进行
     */
    enq(node);
    return node;
}
  • enq 方法
// 1. 如果头尾节点还未初始化 保证 head 和 tail 节点正确初始化 , 
// 2. 如果已经初始化 则 保证 CAS操作尾部节点 正确被替换当前node节点
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        // 1. AQS中的头尾节点未初始化 那么执行初始化操作
        if (t == null) { // Must initialize
            // new Node() 是构建一个 伪节点 , 作为 head 和 tail
            // 伪节点是为了 监控AQS中后续节点的状态
            if (compareAndSetHead(new Node()))
                tail = head;
        } 
        // 2. 初始化过了 , 死循环 则保证通过CAS 替换尾节点成功
        else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
  • addWaiter 结论
    • 如果AQS还没有初始化 head、tail节点 则会初始化
    • AQS初始化时 头节点是一个 伪节点 , 用于后续节点的监控作用(在当前方法还不涉及此操作 , 后续会有的)
    • 通过 enq() 方法 内部的死循环方式 , 一定会将当前接入的节点放到 AQS 的队列队尾
4. acquireQueued : 判断当前线程是否还能够再次尝试获取锁资源 , 如果不能获取锁资源 或者 有没有获取到锁资源 则就尝试将当前线程挂起
  • acquireQueued 方法
/**
 *  当执行到 acquireQueued 方法 , 则表示 当前线程没有拿到锁资源 , 并且已经将线程封装为Node添加到AQS队列中进行排队了
 *  使用lock方法执行下来的操作 在此方法中 不需要考虑中断操作
 * @param node 就是 addWaiter 的产物 , 即返回当前线程的Node节点
 */
final boolean acquireQueued(final Node node, int arg) {
    // failed 默认没有拿到锁资源 (这个属性 真正起作用的是 使用 tryLock() 和 lockInterruptibly() 方法)
    boolean failed = true;
    try {
        // 中断标记位
        boolean interrupted = false;
        
        for (;;) {
            // 拿到当前node 的前驱节点 , 当前节点的上一个节点不可能是null
            final Node p = node.predecessor();
            /**
             * 1.(判断当前节点是否有资格抢占锁资源) : 如果上一个节点是 头节点 , 那么当前节点就是第一位Node , 那么当前线程就尝试获取锁资源
             *   不论是 公平锁 还是非公平锁 如果是排在第一位的Node 那么都可以尝试获取锁资源
             */
            if (p == head && tryAcquire(arg)) {
                // 进入 if 说明 当前节点是第一个节点 并且获取到了锁资源
                // 那么当前节点在AQS中也就没有排队意义了 , setHead 将当前节点设置为新的伪节点
                setHead(node);
                // 清除原来的 head 节点 的 next 指针 , 帮助GC
                p.next = null; // help GC
                // 拿到锁资源 failed 变为 false
                failed = false;
                return interrupted;
            }
            /**
             *  1. 到这里 - 说明当前线程没有资格抢占锁资源 , 或者没有抢到锁资源
             *  2. shouldParkAfterFailedAcquire 是基于上一个节点的状态 , 来判断当前节点是否可以执行挂起操作
             *     如果上一个节点状态是 SIGNAL(-1) , 那么就返回true , 如果不是则返回false , 继续下次循环
             *     还有一种情况 , 如果前边都是 已经处于取消状态(CALCELLED ==> 1)的节点 那么就需要将当前节点移动到适合的位置[pred.waitStatus > 0](状态为-1或者0的节点后边)
             */
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()){
                interrupted = true;
            }
        }
        
        } finally {
            /**
             * 获取锁资源失败 或者抛出异常 才会执行此if语句 , 在 tryLock 和 lockInterruptibly 方法才会涉及到此逻辑
             * 执行线程等待操作时 抛出异常才会走这里
             */
            if (failed)
                cancelAcquire(node);
        }
}
  • setHead 方法
// 当前节点的线程已经 拿到了锁资源 , 那么就把当前head 变为node节点 , 这个node就成为了新的**伪节点**
private void setHead(Node node) {
    // 将 node 变为新的伪节点逻辑
    head = node;
    node.thread = null;
    node.prev = null;
}
  • 方法 shouldParkAfterFailedAcquire
/**
 * 当前Node没有拿到锁资源 或者 没有资格竞争锁资源 , 那么就尝试看能不能挂起线程 
 * 
 * AQS双向链表中 , Node 之间有个很有趣的关系 , 比如到这个方法 当前的node想要挂起操作
 * 但是需要确保 node 前边的节点是一个正常的节点(state != 1 , 即不是中断的状态) , 如果前边的节点不正常 那么node执行挂起之后,没有节点可以去唤醒这个node了
 * 也即node将一直处于挂起状态
 * 所以当前节点需要在AQS队列的队尾处向前 遍历 
 *      如果找到前边状态是 0  , 那么就通过 CAS 改变其状态为 -1
 *      如果前边的节点状态是 SIGNAL==>-1 才行 , 然后把当前node移动到找到的这个节点后边
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    /** 
     *  Node节点的状态:
     *   1 : CANCELLED(取消) , 表示当前节点已经取消
     *   0 : 默认的Node节点状态
     *  -1 : SIGNAL(信号) , 表示当前节点的后继节点可以直接挂起
     *  -2 : CONDITION(条件) : 当在使用 Condition进行await、signal操作 时才会涉及到的状态
     *  -3 : PROPAGATE(传播) : 在涉及到 共享锁时 才会使用的状态
     *
     *   Node节点的状态 : 只要不是 CANNELLED(1) , 那么节点就是正常的
     */
    // 获取前一个节点的 waitStatus 属性
    int ws = pred.waitStatus;
    // 只有上一个节点状态是 SIGNAL(-1) 那么当前节点可以执行挂起操作
    if (ws == Node.SIGNAL)
        return true;
    /**
     * 1. 上一个节点的状态 > 0 只能是 CANCELLED 取消的状态 
     *    那么这时 当前线程是不允许执行挂起的 , 否则将会出现永久休眠的状态
     * 2. 所以需要想办法 , 将当前节点 前驱指针 移动到 不是 CANNELLED 状态的后边
     *    所以就一直 往前循环找正常的Node(状态不是1的都行)
     * 3. 找到后 将当前节点的 前驱指针 指向找到的符合要求的节点
     */
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
            /**
                // 上边代码也就相当于是 
                // 先获取 前一个节点的前一个节点
                pred = pred.prev;
                // 再把 当前节点的前驱指针指向 pred
                node.prev = pred;
                // 然后再判断本次找到的 pred 节点是否符合要求 (状态不是 CANNELLED(1 ==> 也即 > 0)) , 不符合继续往前找
                // 最坏的结果就是 从尾节点一直找到 头节点(伪节点) , 伪节点的状态是不可能是取消状态的
             */
            
        } while (pred.waitStatus > 0);
        // 找到符合标准的 前驱节点 pred 后 将 pred的next后继指针指向 当前node节点
        pred.next = node;
    } else {
        /**
         *  到这个 else 说明前一个节点的状态 不是 SIGNAL(-1) 或着 CANCELLED(1) 而是 0 默认的状态
         *  那么就表示当前节点是正常的 , 那么就通过CAS设置 这个前一个节点的状态为 SIGNAL 状态 , 可以满足后边的节点执行挂起状态
         *  先结束 acquireQueued 里的 for(;;) 循环一次 再次进入就能看到前一个节点状态是 SIGNAL , 那么就可以执行挂起操作了
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
  • parkAndCheckInterrupt
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    /**
     *  这个方法可以确认 , 当前线程 是正常唤醒的 还是 被中断唤醒的
     *  如果是中断唤醒 那么就返回true了 , 正常唤醒返回false
     */
    return Thread.interrupted();
}
  • LockSupport - park
/**
 * LockSupport.park 方法 , 会通过 Unsafe类 通过native方法 将线程从 RUNNING 状态 转变为 WAITING 状态
 * 所以需要使用 LockSupport.unpark() 方法来唤醒被park方法处于WAITING状态的线程 
 */
public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(false, 0L);
    setBlocker(t, null);
}
3.2.2 tryLock 方法
1. tryLock() , 仅仅是尝试获取锁资源 , 其他什么操作都不做
  • tryLock
/**
 * 公平锁和非公平锁 调用tryLock 都是这个方法
 */
public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
}
  • tryLock内部调用
/**
 * 这里也就是 上边分析 lock 方法时 非公平所的 尝试获取锁资源的逻辑哦 
 * 1. 看state的值 如果是0 , 则使用CAS尝试获取当前锁资源
 * 2. 如果state不是0 就看当前线程是不是持有锁资源的线程 , 执行锁重入操作
 * 3. 如果没有抢到锁资源 或者 不是锁重入 那么直接返回false
 * 
 */
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
  • tryLock 方法结论
    1. 不论是公平锁还是非公平锁 , 调用 tryLock 后都按照 非公平锁的 抢占锁资源的逻辑执行
    2. tryLock方法 仅仅是 尝试获取锁资源 , 并不会加入AQS等操作
      1. 拿到就拿到了 , 拿不到也没关系 不对加入AQS队列中
2. tryLock(long time, TimeUnit unit)
  • 概述

设置一个时间 在这个时间内 线程会一直在AQS队列中等待设置的时间
当时间到了之后再次尝试获取锁资源 如果拿不到 则直接返回false
或者在等待过程中如果线程被中断 - 则会抛出中断异常

  1. tryLock(long timeout,TimeUnit unit) 方法
/**
 * 传入 时间长度  
 * 传入的时间最终都会转和时间单位换为 纳秒
 */
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
  1. tryAcquireNanos() 方法
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
    // 判断当前线程的中断标记位是否正常 , 如果线程的中断标记位已经是true了那么就不能往下进行了 直接抛出中断异常
    if (Thread.interrupted()){
        throw new InterruptedException();
    }

    // 1. 线程没有处于中断状态
    // 2. 那么就先尝试获取锁资源(注意区分公平锁和非公平锁) , 拿锁成功直接返回true
    return tryAcquire(arg) ||
        3. 如果tryAcquire拿锁失败 , 那么就需要等待指定时间
        doAcquireNanos(arg, nanosTimeout);
}
  1. doAcquireNanos 方法
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
    // 如果等待时间 <= 0 就相当于和调用 tryLock没区别了 , 直接返回false 拿锁失败
    if (nanosTimeout <= 0L)
        return false;
    // 设置等待的 结束时间 , 纳秒单位
    final long deadline = System.nanoTime() + nanosTimeout;
    // 将当前线程扔到 AQS 队列中
    final Node node = addWaiter(Node.EXCLUSIVE);
    // 拿锁失败 , 默认 true , 即默认是拿锁失败的
    boolean failed = true;
    try {
        for (;;) {
            /**  
             * 这段代码在将 lock方法时 判断当前线程是否具有抢占锁资源资格 和挂起线程那块一样的 acquireQueued
             * 如果在 AQS 中 当前node 是 head的next 直接抢锁 (具有抢占所资源资格)
             */
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                // 只有在这里是 代表拿锁成功的 , 否则 failed 一直都是失败的
                failed = false;
                return true;
            }
            // 计算 剩余的等待时间 , 主要在上一步 判断是否具有抢占资格会消耗时间
            nanosTimeout = deadline - System.nanoTime();
            // 判断是否还需要等待
            // 如果发现上一步消耗的时间比较长 已经超过等待的结束时间 , 那么直接拿锁失败 返回false
            if (nanosTimeout <= 0L)
                return false;
            /**
             *  shouldParkAfterFailedAcquire(之前也讲过的) : 根据上一个节点确定当前节点能否挂起线程 , 并把当前节点移动到可以使其挂起的节点后边
             */
            if (shouldParkAfterFailedAcquire(p, node) &&
                    // spinForTimeoutThreshold = 1000L , 1000纳秒 
                    // 如果剩余时间太短 , 那么就不用挂起了 , 太短的话 执行挂起的时间就已经结束等待了
                    nanosTimeout > spinForTimeoutThreshold){
                    /**
                     *  时间不短(足够) , 那么就将当前线程 挂起剩余的时间 
                     *  parkNanos 将线程处于 TIME_WAITING 线程会自动唤醒 
                     *  parkNanos 方法处理过的线程有两种唤醒方式 
                     *      1. 时间到了 自动唤醒
                     *      2. 等待过程中 ,其中断标记位被改为了true , 那么也会唤醒 - (会抛出中断异常)
                     */
                    LockSupport.parkNanos(this, nanosTimeout);
            }
            // 这里就是判断 当前线程 是不是被中断唤醒的 , 如果是 则抛出中断异常
            if (Thread.interrupted())
                // 证明是中断唤醒的
                throw new InterruptedException();
            }
    } finally {
        if (failed)
            // 如果拿锁失败 , 那么就走把当前在AQS队列中 等待的当前节点node取消掉
            cancelAcquire(node);
    }
}
  1. cancelAcquire 取消某个节点操作
/**
 * 当 AQS 队列中 有的节点内的线程 被中断了 或者 , 那么就会执行这个方法 执行取消操作 
 * 取消节点的整体操作流程: 
 *  1. 需要把当前取消的 node 节点的 thread 属性设置为null
 *  2. 以当前需要取消的节点开始 , 向AQS队列前找 , 找到有效节点 (节点状态不是 1 取消的) , 找到后就把自己的指针和找到的节点关联上 
 *        然后后边才会取消自己操作 , 会把向前找过程中已经是中断状态的节点跳过去
 *  3. 将当前节点的 状态 设置为 1 , 代表当前节点是 取消的
 *  4. 然后将当前节点 脱离 AQS 队列时会有三种情况
 *      4.1 当前 node 是 tail 节点 , 从node向前找到有效节点 后 直接将 tail 指向该有效节点即可 , 然后设置这个有效节点的 next 节点为 null
 *      4.2 如果当前取消的节点是 head 的后继节点 , 那么就需要 去唤醒当前节点的 后继节点
 *      4.3 如果取消的节点 不是尾节点 也不是 头节点的后继节点 , 那么就需要处理前边和后边的节点的指针指向问题
 *      
 * @param node , 需要执行取消的node节点
 */
private void cancelAcquire(Node node) {
    // 当前节点为 null 直接忽略即可
    if (node == null)
        return;
    // 将当前取消节点的 thread 属性设置为 null
    node.thread = null;

    /**
     * 这块代码 , 就是以当前node开始 向前找到有效的节点 , 跳过中间 已经是取消状态的节点
     * pred: 最终表示的就是 有效节点 Node
     */
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;
    
    // predNext : 表示的就是上边找到有效节点后 , 原始的有效节点的后继节点 , 即 oldValue 快照 
    Node predNext = pred.next;
    // 将当前取消节点的状态 设置为 CANCELLED ==> 1 , 取消状态
    node.waitStatus = Node.CANCELLED;
    
    // ======================================
    // 下边的代码 就是 将当前 node 脱离AQS队列的逻辑
    // ======================================
        
    /**
     *  这里即 是第一种情况 , 要取消的节点是尾节点 , 那么直接将 tail 指针指向有效节点即可
     */
    if (node == tail && compareAndSetTail(node, pred)) {
        // 将有效节点的 next 指针通过CAS的方式 设置为 null
        // 有效节点的next指针的 原始值 就是 predNext
        compareAndSetNext(pred, predNext, null);
    } 
    // 到这里表示 取消的节点可能不是尾节点(不排除 CAS 设置尾节点并发失败了的情况) , 这里要讨论剩下的两种情况
    else {
        int ws;
        // 不是head的后继节点
        if (pred != head &&
            /**
             *  拿到上一个节点的状态 并且判断上一个节点的状态为 -1 , 则说明后面的线程可以执行挂起操作
             *  如果上一个节点状态不是 -1 并且 也不是取消状态 , 那么就把其状态改为-1
             */
            ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) 
            && pred.thread != null) {
            /** 上面的这一串判断 - 都是为了避免后边的节点不能被唤醒 , 所以一定会把 当前有效节点的状态改为 -1 这样才能保证后边节点处于挂起状态的线程被唤醒 **/
            // 不需要担心后边的节点没有挂起 还要执行唤醒操作 , 执行唤醒操作时会进行判断
            /**
             * 到这说明 pred 节点是有效节点 
             * 那么就 替换 pred 的 next 节点为 当前node节点的下一个节点 , 把当前节点绕过去
             */
            // 当前节点的下一个节点
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                // 将当前 有效节点的 next 指针指向 当前node节点的下一个节点 , 跳过node自己
                compareAndSetNext(pred, predNext, next);
        } else {
            /**
             *  没有通过上边判断 - 这里说明 , 当前节点node 是head的后继节点 , 那么就尝试唤醒后继节点
             *  这个方法到 锁资源释放再说 , 锁资源释放的时候也需要进行后续节点的唤醒操作 也是这个方法
             */
            unparkSuccessor(node);
        }
        
        // 将当前节点的 下一个指针指向自己 , 变成不可达对象 , 帮助GC回收
        node.next = node; // help GC
    }
}
3.2.3 lockInterruptibly 方法
  • 概述

这个方法和 tryLock(time,unit) 方法类似 , 只不过这个方法会直至等待拿到锁资源为止 , 除了被中断抛出中断异常而停止

  • lockInterruptibly
public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}
  • acquireInterruptibly
public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}
  • doAcquireInterruptibly
/**
 *  这个方法就是 和 tryLock(time,unit) 方法的唯一区别 
 *  
 *  这个方法 拿不到锁资源 , 就死等锁资源 
 *      只有等到所释放时被唤醒 , 或者是被中断唤醒
 */
private void doAcquireInterruptibly(int arg) throws InterruptedException {
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                // 这个方法上边有解释 -parkAndCheckInterrupt() , 就是判断是否正常唤醒的
                parkAndCheckInterrupt())
                // 不是正常唤醒的 就抛出 中断异常
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

四、释放锁流程源码剖析

4.0 释放锁的大概流程

  1. 释放锁资源 , 其实就是调用 tryRelease() 方法了
  2. 首先要判断是不是当前线程持有的锁资源 , 不是当前线程持有的锁资源让其去释放锁 不太合适 ,
    1. 不是的话就抛异常 , 阻止执行后边流程
    2. 是的话就执行逻辑 , 需要对 属性state - 1 操作
      1. 如果不是0 , 那么本次释放就结束了 方法结束
      2. 如果-1之后 state == 0 那么证明锁资源释放干净了(锁重入几次就需要释放几次)
        1. 释放干净之后 , 就先查看头节点(伪节点)状态 , 头节点的状态 waitStatus 是否不为0(是否为-1)
        2. 如果是0 那么就表示 AQS 队列中没有挂起的线程
        3. 如果是-1 那么就表示 后续 AQS 队列中 有挂起的线程 需要唤醒
          1. 唤醒线程时 , 需要先将当前节点的状态改变为0 , 然后去后边找有效的节点执行唤醒(不能去唤醒取消的节点 唤醒取消的节点没有意义) , 找到之后唤醒线程即可
          2. 寻找有效节点是 从后向前找的
            image.png

4.1 unlock() , 释放锁操作 不区别公平锁还是非公平锁

  • unlock
public void unlock() {
    // 释放锁资源 不区分公平锁和非公平锁
    sync.release(1);
}
  • release
public final boolean release(int arg) {
    // tryRelease 核心释放锁资源的操作之一
    // 如果没有抛出异常 , 返回 true 则说明锁重入操作已经释放干净 , 如果返回false 说明锁重入还未释放干净
    if (tryRelease(arg)) {
        Node h = head;
        /**
         *  h 如果是 null , 那么只能说明 AQS 根本就没有初始化 , 也就是当前锁资源 一直没有出现过排队的现象 一直都是一个线程在拿锁和释放锁 , 也就没必要去执行后边的操作了
         *  反之 , 需要判断伪节点的状态是否是 -1 (ReentrantLock中只能出现-1了)
         *  如果状态是-1 , 那么就需要执行唤醒操作了
         *  如果没有那么就直接返回true 表示释放成功了
         */
        if (h != null && h.waitStatus != 0)
            // 执行唤醒后续节点操作
            unparkSuccessor(h);
        return true;
    }
    return false;
}
  • tryRelease
// ReentrantLock 的释放锁资源操作
protected final boolean tryRelease(int releases) {
    // 拿到锁资源 对state属性-1 , 拿到结果的快照 还并未写回
    int c = getState() - releases;
    // 判断当前持有锁的线程 是否是当前线程 , 如果不是就直接抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 代表当前锁资源是否已经释放干净了
    boolean free = false;
    // 证明释放干净了
    if (c == 0) {
        free = true;
        // 设置持有锁的线程设置为 null
        setExclusiveOwnerThread(null);
    }
    // 最后写回 state 属性
    setState(c);
    // 锁资源释放干净 返回true , 否则返回false
    return free;
}
  • unparkSuccessor , 唤醒后续节点方法
/**
 * 唤醒后边排队的线程 (传入的节点一定是 head 节点 , 从head节点开始唤醒)
 * 
 * 找有效节点的方式是从后往前找的 , 需要直到 addWaiter 添加节点的大致流程,就知道 添加时先把节点的prev指针指向原来的tail节点 , 然后把tail指向新增的node
 * 这样可以避免出现 不能唤醒到新增的节点
 */      
private void unparkSuccessor(Node node) {
    // 拿到头节点的状态
    int ws = node.waitStatus;
    // 头节点的状态是 -1 , 那么就先将其状态改为 0 
    if (ws < 0){
        // 通过CAS操作改变头节点的状态
        compareAndSetWaitStatus(node, ws, 0);
    }
    // s : 头节点的后继节点
    Node s = node.next;
    // 如果后续节点是 null , 或者 后续节点的状态 > 0 即取消状态(1) , 就需要在AQS中找到有效节点去唤醒了
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 从后往前找的原因 , 新进来的节点 在tail指向新节点之前 新节点的prev就已经指向了前一个节点 (可详看 addWaiter(node) 方法) , 这样会找到AQS中的所有节点
        // 从前往后找的话 就有概率出现 不能被唤醒的节点
        // **从后往前** 找到AQS中的有效节点 , 一直找到离当前node最近的有效节点 (从 t != node 看出来) , 将其给到 s
        for (Node t = tail; t != null && t != node; t = t.prev)
            /**
             * 从后往前找,找到了离node最近的 并且是有效的节点 , 赋值给 s
             * 每次找到有效的节点 都会把 其节点内存储的 thread 赋值给 s , 直到t指针遍历到 node 本身时
             */
            if (t.waitStatus <= 0)
                    s = t;  
    }   
    // 找到了 正常的唤醒节点 , 就去唤醒该线程
    if (s != null)
        LockSupport.unpark(s.thread);
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值