AQS-Lock获取锁操作

获取锁

获取锁最直观的感受就是使用 Lock.lock () 方法来获得锁,最终目的是想让线程获得对资源的访问权。 Lock 一般是 AQS 的子类,lock 方法根据情况一般会选择调用 AQS 的 acquire 或 tryAcquire 方法。acquire 方法 AQS 已经实现了,tryAcquire 方法是等待子类去实现,acquire 方法制定了获取锁的框架,先尝试使用 tryAcquire 方法获取锁,获取不到时,再入同步队列中等待锁。tryAcquire 方法 AQS 中直接抛出一个异常,表明需要子类去实现,子类可以根据同步器的 state 状态来决定是否能够获得锁,接下来我们详细看下 acquire 的源码解析。前面讲过,AQS 包含两种模式,独占模糊和共享模式,即通常讲的排它锁和共享锁。

获取排他锁

尝试获取锁

// 排它模式下,尝试获得锁
public final void acquire(int arg) {
    // tryAcquire 方法是需要实现类去实现的,实现思路一般都是 cas 给 state 赋值来决定是否能获得锁
    if (!tryAcquire(arg) &&
        // addWaiter 入参代表是排他模式
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

其中 AQS 并没有 对 tryAcquire 方法做一个实际实现,就给了子类实现,其中包括:ReentrantLock、ReentrantReadWriteLock 锁都对 tryAcquire 做了自己的实现。

总体上获取排他锁可以分为下面四个步骤:

1、尝试执行一次 tryAcquire,如果成功直接返回,失败走 2;
2、线程尝试进入同步队列,首先调用 addWaiter 方法,把当前线程放到同步队列的队尾;
3、接着调用 acquireQueued 方法,两个作用,1:阻塞当前节点,2:节点被唤醒时,使其能够获得锁;
4、如果 2、3 失败了,打断线程。

加入等待队列

很显然,按照之前讲过的思路,获取不到锁资源的线程是需要被加入到队列中的,并且被阻塞起来。addWaiter 实现了将线程节点放到队列中。

private Node addWaiter(Node mode) {
    // 新建一个线程节点
    Node node = new Node(Thread.currentThread(), mode);
    // 获取队尾线程节点
    Node pred = tail;
    // 如果队列不为空,之前已经有线程在队列中排队
    if (pred != null) {
        // 把新线程节点的的前指针指向队尾 
        node.prev = pred;
        // 通过 CAS 操作把 tail 指向新的节点,保证 tail 永远指向最后一个线程节点(队列是在尾部添加线程节点) 
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //自旋保证node加入到队尾
    enq(node);
    return node;
}

初始化队列

显然,以上是在队列不为空时的操作,时直接将新线程节点追加到队列末尾,这很符合队列的操作。那当队列为空时,队列的初始化是在 enq 中实现。

private Node enq(final Node node) {
    for (;;) {
        // 得到队尾节点
        Node t = tail;
        // 如果队尾为空,说明当前同步队列都没有初始化,进行初始化
        // tail = head = new Node();
        if (t == null) {
            if (compareAndSetHead(new Node()))
                tail = head;
        // 队尾不为空,将当前节点追加到队尾
        } else {
            node.prev = t;
            // node 追加到队尾
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

从代码可以看出会先先初始化队列,主要是把 head 和 tail 指向新节点。关于 else 中的操作,我的理解是:这里除了初始化队列之外,还有对 addWaiter 的补偿,因为 compareAndSetTail(pred, node) 没有成功可以进入到 enq 直到成功为止并且返回。因为大部分情况下 compareAndSetTail(pred, node) 一次就会成功。

阻塞线程

在 acquire 函数中已经知道追加节点之后会调用 acquireQueued。而按照思路,线程节点加入队列之后因为是要被阻塞。直到其他获取到锁资源的线程释放资源。

// 返回 false 表示获得锁成功,返回 true 表示失败
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 自旋
        for (;;) {
            // 选上一个节点
            final Node p = node.predecessor();
            // 如果 node 之前没有获得锁,进入 acquireQueued 方法时,发现自己是第一个线程节点,于是尝试获得一次锁;
            // 我的理解是:这是一种幸运机制,有可能节点刚刚加入队列时,刚好锁资源被释放。那么这时候尝试去获取锁就刚好可以拿到
            if (p == head && tryAcquire(arg)) {
                // 获得锁,设置成 head 节点
                setHead(node);
                //p被回收
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }

            // 判断自己是否应该被阻塞,判断的标准在 shouldParkAfterFailedAcquire 方法中
            // parkAndCheckInterrupt 阻塞当前线程,如果满足条件,节点对应的线程就被阻塞
            if (shouldParkAfterFailedAcquire(p, node) &&
                // 线程是在这个方法里面阻塞的,醒来的时候仍然在无限 for 循环里面,就能再次自旋尝试获得锁
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 如果获得node的锁失败,将 node 从队列中移除
        if (failed)
            cancelAcquire(node);
    }
}

整个方法主要做两件事情:
1:通过不断的自旋尝试使自己前一个节点的状态变成 signal,然后阻塞自己;
2:获得锁的线程执行完成之后,释放锁时,会把阻塞的 node 唤醒,node 唤醒之后再次自旋,尝试获得锁。

总结一下,acquire 方法大致分为三步:

1、使用 tryAcquire 方法尝试获得锁,获得锁直接返回,获取不到锁的走 2;
2、把当前线程组装成节点(Node),追加到同步队列的尾部(addWaiter);
3、自旋,使同步队列中当前节点的前置节点状态为 signal 后,然后阻塞自己;

获取共享锁

共享锁,因为锁资源在同一个时刻可以被多个线程一起访问,和独占锁不同。找到获取共享锁的入口函数 acquireShared;

尝试获取锁

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

和独占锁相同,还是先尝试获取锁,失败了就去做资源入队操作,tryAcquireShared 并没有一个实际的实现,也是交给了子类做具体实现;在使用中可以知道 信号量(Semaphore)和 计数器(CountDownLatch)都有做了具体的实现。

加入等待队列

核心代码在 doAcquireShared 里面,这是获取共享锁的核心代码。


private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

posted @ 2021-11-29 23:42  yaomianwei  阅读(35)  评论(0编辑  收藏  举报