ReentrantLock 与 AQS 源码分析

ReentrantLock 与 AQS 源码分析

1. 基本结构

?? 重入锁 ReetrantLock,JDK 1.5新增的类,作用与synchronized关键字相当,但比synchronized更加灵活。ReetrantLock本身也是一种支持重进入的锁,即该锁可以支持一个线程对资源重复加锁,但是加锁多少次,就必须解锁多少次,这样才可以成功释放锁。

1. 继承

没有继承任何类,因为很多操作都使用了组合完成。

2. 实现

Lock, java.io.Serializable

??这里着重介绍一下 Lock 接口,接口定义了几个必要的方法,也是在 ReentrantLock 中的重点需要分析的方法。

?? 三类方法:获取锁、释放锁、获取条件。

public interface Lock {
    // 阻塞获取锁,如果获取不到锁就一直等待
    void lock();
    // 可中断获取锁,在获取锁的过程可以被中断,但是 Synchronized 是不可以
    void lockInterruptibly() throws InterruptedException;
    // 非阻塞获取锁,没有获取到锁立即返回
    boolean tryLock();
    // 超时获取锁,没获取到锁等待一段时间
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    // 解锁
    void unlock();
    // 等待唤醒机制的条件
    Condition newCondition();
}

从上面可以看到 Synchronized 和 Lock 的一些重要区别:

  1. Lock 的获取锁的过程是可以中断的,Synchronized 不可以,Synchronized 只能在 wait或同步代码块执行过程中才可以被中断。
  2. 由于 Lock 显示的加锁,锁可以横跨几个方法,也就是临界区的位置可以更加自由。
  3. Lock 支持超时获取锁。
  4. 后面会看到 Lock 还支持公平及非公平锁。
  5. 绑定多个 Condition 条件

3. 主要字段

??很好,这个类的字段非常的少,真正起作用的字段只有一个 “锁” 字段。

    // 同步锁
    private final Sync sync;

?? 这个锁(Sync)是一个继承自 AQS 的抽象内部类,说明一下 AQS (AbstractQueuedSynchronizer) 一般被称为队列同步器,他是并发包中的核心组件,绝大多数锁机制都是采用的这个类来实现的。虽然看到他是一个抽象类,但是你会发现里面没有一个方法是抽象方法,他实现了锁机制中的必要的通用的方法,待会会专门讲这个类。不然 ReentrantLock 没办法说,ReentrantLock 里面的锁操作都是依赖于 AQS。

?? 然后这个锁是有两个子类,分别是 NonfairSyncFairSync 从名字上也可以看出这两个类分别代表了 公平锁非公平锁 。何为锁的公平性? 实际上就是新来的线程需要征用锁必须要要等到先于他到达的线程获取并释放锁。也就是获取锁的过程是按照下来后到的顺序进行的,反之就称为非公平锁。后面我们会看到其实这两种锁不同就在于非公平锁在新线程创建后首先会直接进行锁的获取,如果没有获取到会进行一段时间的自旋,始终没获取到锁才进行等待状态。

?? 一般而言,公平锁开销比非公平锁大,这也是比较符合我们的直观感受。公平锁是需要进行排队的,但在某些场景下,可能更注重时间先后顺序,那么公平锁自然是很好的选择。

?? 好总结一下,在 ReentrantLock 中只维护了一个 “锁” 变量,这个锁是继承了 AQS 同步器,然后这个锁又有两种派生的锁:公平锁,非公平锁。那么 ReentrantLock 实现其实就有两种方式:公平锁,非公平锁。

4. 主要方法概览

  1. ctor-2
  2. lock
  3. lockInterruptibly
  4. tryLock
  5. tryLock(time)
  6. unlock
  7. newCondition

2. 基础并发组件 AQS

1. 基本字段

1. 重要字段

?? AQS 是维护了一个同步队列(双向链表),这个队列里面线程都是需要竞争锁的,没有竞争到的就在同步队列中等待。headtail 就指向队列的首尾。state 是一个标志字段,表示当前有多少线程在临界区。一般来说 state 只能是 0 或 1 但是由于锁是可重入的,所以也有大于 1 的情况。

?? 除了一个同步队列还有 0~n 个等待队列,等待队列就是调用了 await 方法的线程,会被挂到调用了 awaitcondition 上面的等待队列,所以有多少个 condition 就有多少等待队列。

    //同步队列头指针
    private transient volatile Node head;
    // 同步队列尾指针
    private transient volatile Node tail;
    // 状态标志,0 则没有线程在临界区,非零表示有 state 个线程在临界区(由于锁可重入)
    private volatile int state;

2. Node 节点

??Node 节点也就是上文所提到的 同步队列等待队列 中的元素,注意两个队列之间的元素类型是一样的因为他们之间会有相互移动转换的动作,这两个队列中的元素自然是线程,为了方便查找和表示 AQS 将线程封装到了 Node 节点中,构成双向队列。

static final class Node {
        // 共享非 null/独占为 null
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;

        /**
         * 线程状态
         */
        static final int CANCELLED =  1;
        static final int SIGNAL    = -1;
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;
        volatile int waitStatus;
       // 双向链表  这两个指针用于同步队列构建链表使用的   下面还有一个 nextWaiter 是用来构建等待单链表队列
        volatile Node prev;
        volatile Node next;
        // 线程
        volatile Thread thread;
        // 等待队列单链表
        Node nextWaiter;

        /**
         * Returns true if node is waiting in shared mode.
         */
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

    }

?? 可以看到上面有一个 waitStatus 属性,代表了线程当前的状态,状态标识就是那些常量。具体如下:

  1. SIGNAL: 正在执行的线程结束释放锁或者被取消执行,他必须唤醒后续的状态为 SIGNAL 节点
  2. CANCELLED: 在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点, 其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化。
  3. CONDITION: 该标识的结点处于等待队列中(不是同步队列),结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
  4. PROPAGATE:在共享模式中,该状态标识结点的线程处于可运行状态。
  5. 0:代表初始化状态。

?? 可以看到,Node 里面的主要字段就是一个状态标志位、一个线程的引用、用于构建链表的指针。注意,有三个指针,其中前两个 nextpre 是用来构建同步队列的(双向链表),后面 nextWaiter 是用来构建等待队列。所以说虽然同步队列和等待队列使用的同一个数据类型,数据结构是不同的,并且在后面我们会看到等待队列中的节点只有两种状态 ConditionCANCELLED 前者表示线程已结束需要从等待队列中移除,后者表示条件结点等待被唤醒。

??下面画图说明一下同步队列和等待队列的情况。

等待队列

同步队列

3. ConditionObject

?? 这个内部类是等待唤醒机制的核心,在他上面绑定了一个等待队列。在这个类中使用了两个指针( firstWaiter/lastWaiter )指向队列的首尾。这里主要看一下 awaitsignalsignalAll 方法。

  1. await

    ?? 当一个线程调用了await()相关的方法,那么首先构建一个Node节点封装当前线程的相关信息加入到等待队列中进行等待,并释放锁直到被唤醒(移动到同步队列)、中断、超时才被队列中移出。被唤醒后的第一件事是抢锁和检查是否被中断,然后才是移除队列。被唤醒时候的状态应该为 SIGNAL ,而在方法中执行的移除队列的操作就是移除状态非 Condition 的节点。

public final void await() throws InterruptedException {
            // 等待可中断
            if (Thread.interrupted())
                throw new InterruptedException();
            // 加入等待队列, new 新的 Node 做一个尾插入
            Node node = addConditionWaiter();
            // 释放当前线程的锁,失败则将当前线程设置为取消状态
            int savedState = fullyRelease(node);

            int interruptMode = 0;
            // 如果没在同步队列就让线程等待也就是看是否被唤醒
            // 如果有中断或者被唤醒那么退出循环
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // 运行到此处说明已经被唤醒了,因为结束了循环
            // 唤醒后,首先自旋一下获取锁,同时判断是否中断
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            // 清理队列中状态不是 Condition 的的任务,包括被唤醒的 SIGNAL 和 被取消的 CANCELLED
            if (node.nextWaiter != null)
                unlinkCancelledWaiters();
            //被中断 抛异常
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }
  1. signal/doSignal/signalAll

    ?? 执行 signal 首先进行锁的判断,如果没有获取到独占锁就直接抛出异常。这也就是为什么只有拥有锁的线程才能执行 signal ,然后获取等待队列中的第一个节点执行 doSignal。

        public final void signal() {
            // 获取独占锁
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            // 唤醒等待队里中的第一个线程
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }

?? doSignal 方法主要就干了三个事 :

  1. 将被唤醒的节点从等待队列中移除(while 循环体),如果被唤醒的节点被取消了就继续唤醒后面的节点(transferForSignal 返回 false)
  2. 否则把这个节点加入到同步队列 ( enq 方法 )
  3. 当同步队列中当前节点的前驱被取消或者没办法唤醒时则唤醒这个线程 ( unpark ),这时候调用了 unpark 正好和 await 中的 park 相对应使得 await 的线程被唤醒,接着执行循环体判断自己已经被移入到同步队列了,接着就可以执行后面的获取锁的操作。
 private void doSignal(Node first) {
            do {
                // 头指针指向唤醒节点的下一个节点,并顺便判断等待队列是否空
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                // 解除引用
                first.nextWaiter = null;
            } while (!transferForSignal(first) && (first = firstWaiter) != null); //移入同步队列失败则继续唤醒下一个线程,否则唤醒成功
            // 唤醒成功的线程不一定马上能开始执行,只有在前驱节点被取消或者没办法被唤醒时
   }

   //  将节点从等待队列移动到同步队列   成功返回 true 失败 false
    final boolean transferForSignal(Node node) {
        // 在等待队列中的节点只有 condition 和 cancelled 两种状态,如果状态更新失败说明任务被取消
        // 否则更新为初始状态   直接返回的话上面的 doSignal 就会继续唤醒后面的线程
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
        // 把当前节点加入同步队列
        Node p = enq(node);
        // 获取同步队列中倒数第二个节点的状态,当前节点的前驱
        int ws = p.waitStatus;
        // 如果前驱节点被取消或者在设置前驱节点状态为Node.SIGNAL状态失败时,唤醒被通知节点代表的线程
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

    // 插入一个节点到同步队列,如果同步队列是空的则加入一个空节点做为头结点
    // 死循环保证肯定能插入    返回插入节点的前驱
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                // 这一步不需要 cas 是因为并发没关系,只是指向链表结尾,不会多线程更新问题
                node.prev = t;
                // 可能有多个线程抢
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

?? 有一个小问题,就是在某个线程中执行了别人的 signal 不会导致当前线程立即放弃锁,之所以会这样正是由于 ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL) 这个判断,即前驱线程都结束了。比如下面的例子:

package util.AQSTest;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

// test signal 执行后不会导致当前线程立即释放锁
public class AQSTest {
    static Lock lock = new ReentrantLock();
   static Condition run1Cond = lock.newCondition();
    static Condition run2Cond = lock.newCondition();

    static class Runner1 implements Runnable {
        @Override
        public void run() {
            lock.lock();
            try {
                System.out.println("runner 1 start");
                run1Cond.await(1, TimeUnit.SECONDS);
                run2Cond.signal();
                System.out.println("runner 1 exit");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }

        }
    }

    static class Runner2 implements Runnable {
        @Override
        public void run() {
            lock.lock();
            try {
                System.out.println("runner 2 start");
                run2Cond.await();
                System.out.println("runner 2 exit");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }

        }
    }
    public static void main(String[] args) {
        new Thread(new Runner1(),"runner1").start();
        new Thread(new Runner2(),"runner2").start();
    }
}

输出的结果始终是:

runner 1 start
runner 2 start
runner 1 exit
runner 2 exit

?? 我使用了工具对上面的代码进行了调试,大致说一下流程,顺便用来捋一捋等待唤醒机制。

?? 首先 runner1 启动,获取到锁,打印出 “runner1 start” ,然后调用了 await 方法,此时 runner1 线程就执行了 AQS 中的 ConditionObject 中的 await 方法,该方法首先 new 了一个新的节点,把 runner1 封装到这个节点里面。挂在了 run1Con 的等待队列上,然后执行了释放锁并判断中断。紧接着 runner1 线程执行循环体判断是否被唤醒也就是是否在同步队列,显然这时候不在,就直接调用了 park 方法,执行休眠 1 秒钟操作, park 方法是 native 方法由操作系统实现。在上面线程释放锁的时候执行的操作是 fullyRelease 这个方法调用了 release 方法,而 release 方法中释放了锁之后,会检查同步队列中是否还有以前因为没抢到锁而等待的线程,如果有执行 unparkSuccessor 也就是唤醒同步队列中的后继线程。那么此时 runner2 会被唤醒,唤醒后就去抢锁,获取到 lock 锁后输出了 “runner2 start” ,然后 runner2 线程又会因为调用 await 处于和 runner1 同样的境地,也就是被放入 run2Con 的等待队列。好!此时 runner1 的超时时间到了,就会被 unpark 这个 unpark 是被操作系统调用的,之后继续执行循环体发现超时时间小于等于 0 ,则调用 transferAfterCancelledWait 里面调用了 enq 就是加入同步队列,接着开始竞争锁,开始执行 run2Con 上的 signal 此时 signal 调用 doSignal 先执行 do while 中的循环体,runner2 从 run2Con 的等待队列上移除,然后执行 transferForSignal 里面又调用了 enq 将他加入同步队列,并返回同步队列中的前驱,前驱节点状态不是 Cancelled 或者 可以被置为 SIGNAL 则 signal 方法结束。接着打印了 “runner1 exit” 。接着需要执行 finally 里面的释放锁的操作了,显然 unlock 肯定调用了 release ,而 release 会唤醒同步队列中的后继的线程,那么位于同步队列中的 runner2 之前的 park 状态就会被打断,从而跳出 while 循环,执行获取锁的操作。打印出 “runner2 exit” ,最后释放锁整个程序结束。

?? 现在总算是吧 Condition 的等待唤醒机制弄清楚了。也把 AQS 中的两个内部类的功能都解释完了。接下来就看 AQS 中的方法。

2. 重要方法

  1. get/setState
  2. release/tryRelease/unparkSuccessor/fullyRelease
  3. acquire/tryAcquire/addWaiter/tryQueued
  4. acquireShared
  5. releaseShared

?? 这些属于 AQS 中常用的方法,但是里面的核心方法都是模板方法,也就是说由继承他的子类来实现,所以只能看个大概的逻辑。一会等到讲 ReentrantLock 时再详细说这里面的方法。

3. ReentrantLock 内部类 Sync/fairSync/noFairSync

1. Sync

?? 这三个内部类实际上是继承自 AQS ,也就是说 ReentrantLock 是采用了 AQS 作为自己的核心并发控制组件完成的一系列的锁操作,及等待唤醒机制。

?? 首先看一下 Sync 他是后面两个的父类,他直接继承自 AQS 。AQS 中留了几个比较重要的模板方法 tryAcquire 、tryRelease 。这个方法直接实现了一些在公平锁和非公平锁中的通用操作,也就是释放锁的操作 tryRelease 。

?? tryRelease 的实现很简单,主要就是依赖于 AQS 中的 state 属性,如果state 值减去要释放的信号量为 0 则释放成功,否则失败。

        // 释放锁的公共操作
        protected final boolean tryRelease(int releases) {
            // 释放锁首先就是使用 AQS 中的 state 的值减去信号量 判断是否为0
            // 如果是 0 则表明成功释放锁,独占线程设为 null,否则说明还占用锁
            int c = getState() - releases;
            // 必须获取到锁才能解锁,否则抛异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

2. fairSync

    公平锁执行 lock 操作就是执行了 AQS 中的 acquire(1) 也就是请求一个锁资源。但是注意,在 AQS 中的 acquire 中的 tryAcquire 方法没有实现,所以必须由当前类实现。

    在 tryAcquire 中做的事情就是看是否有代码在临界区。没有则还要看同步队列中是否有线程等待,当只有这一个线程在获取锁的时候才能正常的获取锁,其他情况都失败。
// 公平锁
    static final class FairSync extends Sync {
        final void lock() {
            acquire(1);
        }

        // 没有代码在临界区或者是当前线程的重入 则获取成功,否则失败
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            // 如果当前线程在获取锁的过程没有其他线程在临界区
            if (c == 0) {
                // 如果同步队列中没有等待的线程,就设置 state ,并且当前线程设为独占线程
                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;
        }
    }

3. noFairSync

同理,这个方法也需要实现 lock 和 tryAcquire 操作。在 lock 中直接判断是否有代码在临界区,没有则直接获取到锁,与公平锁不同的是:公平锁还判断了等待队列中是否有等待的线程。有在临界区的情况时执行 acquire 操作。同样的,首先要执行 tryAcquire 如果失败,加入同步队列并自旋获取锁。还是 tryAcquire 的实现,这里又调用了 nonfairTryAcquire。

    // 非公平锁
    static final class NonfairSync extends Sync {
        final void lock() {
            // 如果没有代码在临界区 直接获取锁,独占
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
            // 有代码在临界区则执行尝试获取锁
                acquire(1);
        }

        // 和公平锁中的 tryAcquire 一模一样只是少了关于同步队列中是否有等待线程的判断
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

    final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            // 没有线程获取锁 直接获取到锁  和公平锁中的 tryAcquire 一模一样只是少了关于同步队列的判断
            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;
        }

?? 好了,现在我们 AQS 中的空的核心方法也被子类实现了,那么现在 fairSync 和 noFairSync 就算是一个完整的 AQS 了。此时看一下加解锁的流程。

只说公平锁,因为非公平锁就只是少了一个判断。

  1. 首先 sync 调用 lock 方法,让后 lock 调用了 AQS 的 acquire(1) 也就是获取一个锁资源。
  2. acquire 就先调用 tryAcquire(1) 尝试获取锁,这时候代码又回调到 sync 中的实现的 tryAcquire 方法,这个方法先判断锁是否已经被别的线程使用,然后需要确定没有更早的线程在同步队列等待获取锁,才把当前线程设置为独占线程,并设置 state 值获取锁。但是如果有代码在临界区需要判断是否为当前线程,因为锁是可重入的。如果是当前线程则 state 加上请求锁的个数,返回。
  3. 这时候又回到 AQS 中,如果上面尝试获取锁的过程失败,就需要调用 addWaiter 将当前线程封装成一个独占节点,等待状态默认为 0,并且返回当前节点。
  4. 加入同步队列后,再调用 acquireQueued 方法,当此线程是同步队列中等待的第一个线程则自旋尝试获取锁,毕竟很可能正在执行的线程马上就会释放锁了,再进行休眠不合适。如果自旋获取锁失败则判断节点状态是否为 SIGNAL 然后执行等待操作。
  5. 锁获取成功则把当前节点设置为头结点,把 thread = null
  6. 至此,Acquire 方法执行结束。

然后调用 unlock 方法解锁操作。

  1. 解锁操作就没那么麻烦,首先还是调用到了 AQS 中的 release 方法,这个方法首先尝试解锁当前线程,又回调到了 sync 中的 tryRelease 。
  2. tryRelease 逻辑比较简单,使用 AQS 中的 state 减去释放的资源数,等于 0 代表完全释放,否则释放失败。
  3. 如果 tryRelease 成功执行就要去唤醒同步队列中的后继节点,继续执行。
  4. 至此,release 方法执行完毕。

4. AQS 中的要方法

1. get/setState

??这两个方法主要是对 state 变量的 volatile 的读写,其实里面就就是普通的 get/set 方法。但是注意的一点就是 state 是 volatile 的。

    // 对状态变量的 volatile 读写
    protected final int getState() {
        return state;
    }
    protected final void setState(int newState) {
        state = newState;
    }

2. release/tryRelease/unparkSuccessor/fullyRelease

?? 这几个方法在一起说主要是因为他们之间存在调用链,首先来看 release 这个方法我们在上面也分析了,里面调用了 tryRelease 、unparkSuccessor。 也就是首先调用 tryRelease 来释放当前线程的锁,如果释放成功就调用 unparkSuccessor 来唤醒同步队列中后继节点。其中 tryRelease 是由子类来实现,里面的主要逻辑就是看当前的 state 变量的值在修改过后是否为0 。这里还有一个 fullRelease 主要是在 ConditionObject 中调用的,当执行 await 的操作的时会执行此方法释放锁。

 //  尝试释放锁
    public final boolean release(int arg) {
        // 如果释放锁成功 唤醒同步队列中的后继节点
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

    // 唤醒同步队列中的后继节点
    private void unparkSuccessor(Node node) {
        // node 一般就是当前正在运行的线程
        int ws = node.waitStatus;
        // 当前线程置为初始状态   可以失败
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        // 找到同步队列中的下一个节点
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {  //没有下一个节点或者被取消
            s = null;
            // 从后往前找第一个没有被取消的线程
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        // 唤醒那个线程
        if (s != null)
            LockSupport.unpark(s.thread);
    }

    final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            int savedState = getState();
            if (release(savedState)) {
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

3. acquire/tryAcquire/addWaiter/acquireQueued

这个和上面的一样,在执行了 acquire 后,会去调用子类复写的 tryAcquire 方法,这个方法就是看有否有代码块在临界区,没有的话直接获取锁(非公平锁),设置 state,有的话要判断是不是当前线程能否进行重入操作,否则就获取失败。失败后会调用 addWaiter ,new 一个新的节点加入到同步队列,接着调用了 acquireQueued 如果这个节点是同步队列中的第一个等待的线程(但不是第一个节点,因为第一个节点是 thread=null 的运行中的线程)就自旋一段时间看能否获取到锁。不能则 park 等待。

// 获取锁
    public final void acquire(int arg) {
        // 尝试获取锁 失败则加入同步队列 如果是同步队列中的第一个线程就自旋获取锁
        // 上面的步骤的自旋获取锁阶段,返回的是是否需要中断,所以下面就进行 selfInterrupt
        // tryAcquire 是模板方法,因为对于公平锁和非公平锁获取锁方式不同
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

    // 创建一个节点放入到同步对列中   可传入是否为独占锁   返回当前节点
    private Node addWaiter(Node mode) {
        // 默认的 status 是 0
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            // 把 tail 设置为 node 成功说明没有竞争
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 失败则就说明空队列   创建头结点
        enq(node);
        return node;
    }

     final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 自旋获取锁
            for (;;) {
                // 获取前驱节点
                final Node p = node.predecessor();
                // 如果前驱是空的头结点,那么也就是说当前线程就是队列中的第一个线程 并尝试获取锁  成功的话方法返回中断情况
                if (p == head && tryAcquire(arg)) {
                    // 把当前节点设置为头结点  thread=null 也就可以看做当前线程在运行,所以就不在同步队列
                    setHead(node);
                    // gc
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 如果获取锁失败,检测为 SIGNAL 或者设置为 SIGNAL 然后让此线程等待 等待操作在 parkAndCheckInterrupt 中完成
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            // 失败 取消
            if (failed)
                cancelAcquire(node);
        }
    }

5. 总结

?? 其实到这里 ReentrantLock 已经讲完了,因为他底层全部调用的是 Sync 中的方法,也就是全都是调用了 AQS 中的方法。而 AQS 中的大部分重要的方法都已经看过了。

原文地址:https://www.cnblogs.com/lwen/p/8684307.html

时间: 2024-10-14 19:04:45

ReentrantLock 与 AQS 源码分析的相关文章

AQS源码分析--jdk1.8

JDK1.8 ArrayList源码分析--jdk1.8LinkedList源码分析--jdk1.8HashMap源码分析--jdk1.8AQS源码分析--jdk1.8 AbstractQueuedSynchronizer概述 ??1. AQS是一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架.??2. AQS提供了双向链表.??3. AQS分为共享模式和独占模式.??4.AQS基于volatile内存可见性和CAS原子性操作实现线程间通信操作. AbstractQueuedS

关于java中ReentrantLock类的源码分析以及总结与例子

一,官方描述 关于ReentrantLock的官方描述,英文的就不贴出来了,这里我只贴出我自己翻译的描述: reentrant是一个跟synchronized具有相同行为和语义的持有锁来访问方法和语句的互斥锁,但是reentrant还拥有被扩展的能力. ReentrantLock会被线程拥有并且持续锁定,不会解锁.线程调用lock()方法返回后,则成功持有锁,否则这个锁正在被另一个线程所持有,只能等待另一个线程释放锁,如果当前线程拥有了锁,则调用lock()方法会立即返回,这个状态可以通过isH

Java并发之AQS源码分析(二)

我在Java并发之AQS源码分析(一)这篇文章中,从源码的角度深度剖析了 AQS 独占锁模式下的获取锁与释放锁的逻辑,如果你把这部分搞明白了,再看共享锁的实现原理,思路就会清晰很多.下面我们继续从源码中窥探共享锁的实现原理. 共享锁 获取锁 public final void acquireShared(int arg) { // 尝试获取共享锁,小于0表示获取失败 if (tryAcquireShared(arg) < 0) // 执行获取锁失败的逻辑 doAcquireShared(arg)

java并发编程基础-ReentrantLock及LinkedBlockingQueue源码分析

ReentrantLock是一个较为常用的锁对象.在上次分析的uil开源项目中也多次被用到,下面谈谈其概念和基本使用. 概念 一个可重入的互斥锁定 Lock,它具有与使用 synchronized 相同的一些基本行为和语义,但功能更强大. 名词解释: 互斥 表示同一时刻,多个线程中,只能有一个线程能获得该锁.但是多个线程都可以调用lock方法,只有一个会成功,其他的线程会被阻塞,直到该锁被释放 可重入 模仿synchronized 的语义:如果线程进入由线程已经拥有的监控器保护的 synchro

JAVA AQS源码分析

转自:  http://www.cnblogs.com/pfan8/p/5010526.html JAVA AQS的全称为(AbstractQueuedSynchronizer),用于JAVA多线程的开发,从名称我们也可以看出,它实现了同步的队列,而这个队列是指线程队列.AQS类在java.util.concurrent.locks下面. AQS和CAS作为JAVA5之后非常重要的特性,能在并发应用中提高程序性能,具体要就实际情况使用,因为JVM也在一直优化synchronized关键字,在JA

ReentrantLock源码分析--jdk1.8

JDK1.8 ArrayList源码分析--jdk1.8LinkedList源码分析--jdk1.8HashMap源码分析--jdk1.8AQS源码分析--jdk1.8ReentrantLock源码分析--jdk1.8 ReentrantLock概述 ??1. ReentrantLock是独占锁.??2. ReentrantLock分为公平模式和非公平模式.??3. ReentrantLock锁可重入(重新插入) ReentrantLock源码分析 /** * @since 1.5 * @aut

【JUC】JDK1.8源码分析之ReentrantLock(三)

一.前言 在分析了AbstractQueuedSynchronier源码后,接着分析ReentrantLock源码,其实在AbstractQueuedSynchronizer的分析中,已经提到过ReentrantLock,ReentrantLock表示下面具体分析ReentrantLock源码. 二.ReentrantLock数据结构 ReentrantLock的底层是借助AbstractQueuedSynchronizer实现,所以其数据结构依附于AbstractQueuedSynchroni

ReentrantLock实现原理及源码分析

ReentrantLock是Java并发包中提供的一个可重入的互斥锁.ReentrantLock和synchronized在基本用法,行为语义上都是类似的,同样都具有可重入性.只不过相比原生的Synchronized,ReentrantLock增加了一些高级的扩展功能,比如它可以实现公平锁,同时也可以绑定多个Conditon. 可重入性/公平锁/非公平锁 可重入性 所谓的可重入性,就是可以支持一个线程对锁的重复获取,原生的synchronized就具有可重入性,一个用synchronized修饰

Java并发编程 ReentrantLock 源码分析

ReentrantLock 一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大. 这个类主要基于AQS(AbstractOwnableSynchronizer)封装的 公平与非公平锁. 所谓公平锁就是指 在多个线程的争用下,这些锁倾向于将访问权授予等待时间最长的线程,换句话说也就是先被锁定的线程首先获得锁. 非公平锁正好相反,解锁时没有固定顺序. 让我们边分析源代码边学习如何使用该类 先来看一下构造参数,默认