从 ReentrantLock 看AQS

1 篇文章 0 订阅

1. AQS

AQS全名:AbstractQueuedSynchronizer,是并发容器J.U.C(java.util.concurrent)下locks包内的一个类。它实现了一个FIFO的队列。底层实现的数据结构是一个双向链表

1.1 AQS 属性

  • head(Node):头结点,直接把它当做 当前持有锁的线程 可能是最好理解的
  • tail(Node):阻塞的尾节点,每个新的节点进来,都插入到最后,也就形成了一个链表
  • state(int):这个是最重要的,代表当前锁的状态,0代表没有被占用,大于 0 代表有线程持有当前锁,这个值可以大于 1,是因为锁可以重入,每次重入都加上 1
  • exclusiveOwnerThread(Thread):继承自AbstractOwnableSynchronizer,代表当前持有独占锁的线程,用来判断重入等操作

1.2 Node 属性

队列中每个线程被包装成一个 Node,它主要有以下几个属性:

  • waitStatus(int):等待状态,CANCELLED = 1,代表线程取消了争抢这个锁;SIGNAL = -1,表示当前node的后继节点对应的线程需要被唤醒;CONDITION = -2;PROPAGATE = -3
  • prev(Node):前驱节点的引用
  • next(Node):后继节点的引用
  • thread(Thread):线程本尊
  • nextWaiter(Node):链接到等待条件的下一个节点,或者共享特殊值,在使用共享锁或者 Condition 时用到。因为条件队列仅在处于独占模式时才被访问,所以只需要一个简单的单链即可在节点等待条件时保存节点,然后将它们转移到队列以重新获取。由于条件只能是互斥的,因此使用特殊值来表示共享模式来保存字段

1.3 使用 AQS 时重要的方法

要使用 AQS,主要通过以下几个方法,这些方法都需要子类实现:

  • tryAcquire:尝试以独占模式获取,可用于实现 Lock#tryLock 方法
  • tryRelease:尝试释放独占同步资源,只有获取到同步状态的线程才能调用该方法
  • tryAcquireShared:尝试以共享模式获取
  • tryReleaseShared:共享模式下尝试释放同步资源
  • isHeldExclusively:查看同步器是否被自己独占

2. ReentrantLock 加锁解锁流程

2.1 加锁操作

构造函数:

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

构造函数中,fair 对应是否是非公平锁。

FairSync#Lock & FairSync#tryAcquire:

final void lock() {
    acquire(1);
}

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // state == 0 此时此刻没有线程持有锁
    if (c == 0) {
        // 虽然此时此刻锁是可以用的,但是这是公平锁,既然是公平,就得讲究先来后到,
        // 看看有没有别人在队列中等了半天了
        if (!hasQueuedPredecessors() &&
                // 如果没有线程在等待,那就用CAS尝试一下,成功了就获取到锁了,
                // 不成功的话,只能说明一个问题,就在刚刚几乎同一时刻有个线程抢先了
                // 因为刚刚还没人的,我判断过了
                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;
}

NonfairSync#Lock & NonfairSync#tryAcquire:

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            // 设置当前拥有独占访问权限的线程。
            // 一个null参数表示没有线程拥有访问权限。此方法不会强加任何同步或volatile字段访问
            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;
}

公平和非公平的区别在于 lock 和 tryAcquire 的实现不同。公平锁 lock 时候直接调用 acquire 方法,而非公平锁要先 CAS 抢一下锁才调用 acquire 方法。

AbstractQueuedSynchronizer#acquire:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 如果 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 返回 true 了
        // 说明当前线程需要被中断,这里设置一下线程中断标记
        selfInterrupt();
}

acquire 两者逻辑就相同了,先执行 tryAcquire 抢一下锁,如果抢到锁了,把自己设置为同步器的独占线程;如果抢锁失败,将当前线程封装成 node 后加入阻塞队列中等待前序节点唤醒。tryAcquire 的时候,公平锁只有在没有等待队列时才会去尝试 CAS 抢占锁,而非公平锁会直接去进行一步尝试 CAS 抢锁。

也就是说,在加入阻塞队列前,非同步锁至少要抢 2 次锁,而同步锁只有在阻塞队列为空的时候才会去抢锁,讲究先来后到。

AbstractQueuedSynchronizer#addWaiter:

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    // 如果阻塞队列不为空,可以尝试将节点快速插入等待队列
    // 如果阻塞队列为空或者 CAS 失败则执行常规插入(enq方法)
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 采用自旋的方式入队
    enq(node);
    return node;
}

addWaiter 是将当前线程封装成 Node 后加入阻塞队列中,先 CAS 尝试快速加入一下,这个时候如果阻塞队列不为空并且没有其他线程竞争加入阻塞队列,大概率是成功的;如果 CAS 失败,转而调用 enq 方法,自旋入队,这一步一只有成功后才会返回。

AbstractQueuedSynchronizer#enq:

private Node enq(final Node node) {
    for (; ; ) {
        // 初始化head和tail
        Node t = tail;
        // 队列为空也会进来这里
        if (t == null) { // Must initialize
            // 初始化head节点
            // 原来 head 和 tail 初始化的时候都是 null 的
            // 还是一步CAS,可能是很多线程同时进来
            if (compareAndSetHead(new Node()))
                // 给后面用:这个时候head节点的waitStatus==0, 看new Node()构造方法就知道了
                // 这个时候有了head,但是tail还是null,设置一下,
                // 把tail指向head,放心,马上就有线程要来了,到时候tail就要被抢了
                // 注意:这里只是设置了tail=head,这里可没return哦,没有return
                // 所以,设置完了以后,继续for循环,下次就到下面的else分支了
                tail = head;
        } else {
            /*
             * AQS的精妙就是体现在很多细节的代码,比如需要用CAS往队尾里增加一个元素
             * 此处的else分支是先在CAS的if前设置node.prev = t,而不是在CAS成功之后再设置。
             * 一方面是基于CAS的双向链表插入目前没有完美的解决方案,另一方面这样子做的好处是:
             * 保证每时每刻tail.prev都不会是一个null值,否则如果node.prev = t
             * 放在下面if的里面,会导致一个瞬间tail.prev = null,这样会使得队列不完整。
             */
            node.prev = t;
            // CAS设置tail为node,成功后把老的tail也就是t连接到node。
            if (compareAndSetTail(t, node)) {
                // 极端情况,如果线程执行完 CAS 操作后被 kill 掉,那么链表该节点前驱的后继
                // 就会为 null,所以在 AQS 通知的时候是采用从后往前遍历的,这样才不会有影响
                t.next = node;
                return t;
            }
        }
    }
}

成功加入阻塞队列后,就该执行 AbstractQueuedSynchronizer#acquireQueued 方法了:

final boolean acquireQueued(final Node node, int arg) {
    // 标识是否获取资源失败
    boolean failed = true;
    try {
        // 标识当前线程是否被中断过
        boolean interrupted = false;
        // 自旋操作
        for (; ; ) {
            // 获取当前节点的的前驱节点
            final Node p = node.predecessor();
            // 如果前继节点为头节点,说明排队马上排到自己了,可以尝试获取资源
            // 若获取资源成功,则执行下述操作
            // p == head 说明当前节点虽然进到了阻塞队列,但是是阻塞队列的第一个,因为它的前驱是head
            // 注意,阻塞队列不包含head节点,head一般指的是占有锁的线程,head后面的才称为阻塞队列
            // 所以当前节点可以去试抢一下锁
            // 这里我们说一下,为什么可以去试试:
            // 首先,它是队头,这个是第一个条件,其次,当前的head有可能是刚刚初始化的node,
            // enq(node) 方法里面有提到,head是延时初始化的,而且new Node()的时候没有设置任何线程
            // 也就是说,当前的head不属于任何一个线程,所以作为队头,可以去试一试,
            // tryAcquire 就是简单用CAS试操作一下state
            if (p == head && tryAcquire(arg)) {
                // 将当前节点设置为头节点
                setHead(node);
                // 前继节点已经释放掉资源了,将其next置空,方便虚拟机回收
                // 对于每个头节点, node.thread = null , node.prev = null
                // 把 p.next 指向 null , 则之前的头节点不再含有强引用
                p.next = null; // help GC
                // 标识获取资源成功
                failed = false;
                // 返回中断标记
                return interrupted;
            }
            /*
             * 获取前继节点不是头节点或者获取资源失败
             * 则需要通过 shouldParkAfterFailedAcquire 函数
             * 判断是否需要阻塞该节点持有的线程
             * 若 shouldParkAfterFailedAcquire 函数返回 true
             * 则继续执行 parkAndCheckInterrupt() 函数
             * 将该线程阻塞并检查是否可以被中断,若返回 true,则将 interrupted 标志置于 true
             */
            if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 最终获取资源失败,则当前节点放弃获取资源
        // 什么时候 failed 会为 true?
        // tryAcquire() 方法抛异常的情况
        if (failed)
            cancelAcquire(node);
    }
}

这个方法有个自旋操作,只有当自己是阻塞队列头节点即前驱是 head 时会去尝试抢锁,抢到锁返回中断状态,否则执行 AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire 方法,判断自己是不是应该挂起:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    /*
     * 如果 ws 的值为 -1,说明前继节点完成资源的释放或者中断后,会通知当前节点的
     * 回去等通知就好了,不用自旋频繁地打听消息
     */
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    /*
     * 如果前继节点的 ws 值大于0,即为1,说明前继节点处于放弃状态(cancelled)
     * 那就继续往前遍历,直到当前节点的前继节点的 ws 值为 0 或负数
     * 直到满足 if(p == head && tryAcquire(arg)) 条件,acquireQueued方法才能够跳出自旋过程
     *
     * 前驱节点 waitStatus大于0 ,之前说过,大于0 说明前驱节点取消了排队。
     * 这里需要知道这点:进入阻塞队列排队的线程会被挂起,而唤醒的操作是由前驱节点完成的。
     * 所以下面这块代码说的是将当前节点的prev指向waitStatus<=0的节点,
     */
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         * 将前继节点的ws值设置为Node.SIGNAL,以保证下次自旋时,
         * shouldParkAfterFailedAcquire直接返回true
         *
         * 每个 node 在入队的时候,都会把前驱节点的状态改为 SIGNAL,
         * 然后阻塞,等待被前驱唤醒
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

2.2 解锁操作

正常情况下,如果 shouldParkAfterFailedAcquire 返回 true,即需要挂起,会调用 parkAndCheckInterrupt 方法进行挂起操作,等待被唤醒

ReentrantLock#unlock

public void unlock() {
    sync.release(1);
}

AbstractQueuedSynchronizer#release

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

回到 ReentrantLock.Sync#tryRelease 方法:

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 这里就是重入问题,只有当 state == 0 了,才能释放同步资源
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

tryRelease 返回 true,即完全释放掉同步资源后,调用 unparkSuccessor 唤醒阻塞队列中离头节点最近的非取消节点的线程:

private void unparkSuccessor(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;
        // 从后向前遍历,找到离node最近的非取消节点,避免在node==tail时再次有入队节点
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

被唤醒的线程醒来后又回到这个方法:acquireQueued(final Node node, int arg),这个时候,node的前驱是head了,它也就可以拿锁了。到这里,从加锁到解锁唤醒就闭环了。

2.3 ReentrantLock 执行过程示例

首先,假设有两个线程获取锁。第一个线程调用 reentrantLock.lock( ) 方法,此时没有任何线程占用锁,tryAcquire(1) 直接返回 true,获取到锁,结束(此时只是设置了 state =1,连 head 都没有初始化,也就没有阻塞队列)

此时状态:

+----- AQS ----+
|  state(1)    | 
|  head(null)  |
|  tail(null)  |
|  exThread(1) |
+--------------+

线程 1 没有调用 unlock( ) 之前,线程 2 调用 Lock 获取锁。线程 2 会初始化 head(new Node( ),此时 head 的 waitStatus = 0)

+----- AQS ----+      +---------+ 
|  state(1)    | head | ws(0)   | tail
|  exThread(1) |      | th(null)|
+--------------+      +---------+

之后线程 2 也会插入到阻塞队列中挂起。线程 2 执行 shouldParkAfterFailedAcquire 方法判断自己是否需要挂起时,会将 head 的 waitStatus 设置为 -1,表示等待 head 的通知

+----- AQS ----+      +---------+     +---------+ 
|  state(1)    | head | ws(-1)  |<----| ws(0)   | tail
|  exThread(1) |      | th(null)|---->| th(2)   |
+--------------+      +---------+     +---------+

线程 1 调用 unlock 释放锁,将独占线程设置为 null、state 设置为 0后,调用 unparkSuccessor 方法,从后往前遍历阻塞队列,找到离头节点最近的非取消节点的线程(线程 2)进行唤醒。

+------ AQS ------+      +---------+     +---------+ 
|  state(0)       | head | ws(-1)  |<----| ws(0)   | tail
|  exThread(null) |      | th(null)|---->| th(2)   |
+-----------------+      +---------+     +---------+

唤醒的线程会继续执行 acquireQueued 方法,获取到锁后将自己设置为头节点,返回中断标记。

+----- AQS ----+      +---------+ 
|  state(1)    | head | ws(0)   | tail
|  exThread(2) |      | th(2)   |
+--------------+      +---------+

说明:

  1. head 一般情况下是获取到线程的节点,head 之后的队列才叫阻塞队列
  2. waitStatus 中 SIGNAL(-1) 状态的意思是,代表后继节点需要被唤醒。

3. CountDownLatch

CountDownLatch在多线程并发编程中充当一个计时器的功能,并且维护一个count的变量,并且其操作都是原子操作。CountDownLatch 基于 AQS 的共享模式的使用,该类主要通过countDown( )和await( )两个方法实现功能的,首先通过建立CountDownLatch对象,并且传入参数即为count初始值。如果一个线程调用了await()方法,那么这个线程便进入阻塞状态,并进入阻塞队列。如果一个线程调用了countDown()方法,则会使count-1;当count的值为 0 时,这时候阻塞队列中调用await( )方法的线程便会逐个被唤醒,从而进入后续的操作。

4. CyclicBarrier

利用CyclicBarrier类可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作。CyclicBarrier字面意思是“可重复使用的栅栏”。

在CyclicBarrier类的内部有一个计数器,每个线程在到达屏障点的时候都会调用await方法将自己阻塞,此时计数器会减1,当计数器减为0的时候所有因调用await方法而被阻塞的线程将被唤醒

CountDownLatch 和 CyclicBarrier 的区别:

  • CountDownLatch 基于 AQS 共享模式实现,CyclicBarrier 基于 Condition 实现
  • CyclicBarrier的计数器由自己控制,而CountDownLatch的计数器则由使用者来控制,在CyclicBarrier中线程调用await方法不仅会将自己阻塞还会将计数器减1,而在CountDownLatch中线程调用await方法只是将自己阻塞而不会减少计数器的值
  • CountDownLatch只能拦截一轮,而CyclicBarrier可以实现循环拦截,一般来说用CyclicBarrier可以实现CountDownLatch的功能,而反之则不能
  • 除此之外,CyclicBarrier还提供了:resert( )、getNumberWaiting( )、isBroken( )等比较有用的方法

5. Semaphore

Semaphore是计数信号量。Semaphore管理一系列许可证。每个acquire方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个release方法增加一个许可证,这可能会释放一个阻塞的acquire方法,经常用于限制获取某种资源的线程数量。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值