通过调试来从源码上理解AbstractQueuedSynchronizer(AQS)

这篇文章首先对AQS进行详细解析,作为上文,还有下文,专门针对几个以AQS为低层进行实现的工具类进行讲解,通过这篇文章,能够对AQS有更好的理解。

AbstractQueuedSynchronizer(AQS)是java并发包中很多并发工具类的基础,比如java.util.concurrent.locks.ReentrantLock重入锁、java.util.concurrent.locks.ReentrantReadWriteLock读写锁、java.util.concurrent.Semaphore信号量、java.util.concurrent.CountDownLatch。它们都是在自己的类中又定义了内部类Sync类继承AbstractQueuedSynchronizer,然后重写其中的一些protected修饰的方法。AQS内部定义了一些模板方法,这些模板方法会调用前面说到的protected修饰的方法。通过模板方法设计模式,所有的这些高级的同步组件都不需要再重写关于线程排队、等待、唤醒等相关代码,这些代码被AQS抽离出来实现了重用。

下面我们从独占锁ReentrantLock入手,来看看AQS内部到底做了哪些事情。首先是一段很简单的代码,实现了三个线程同时调用被ReentrantLock加锁保护的方法sayHello。

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

public class AQSDebug {
    private Lock lock = new ReentrantLock();

    private void sayHello() {
        lock.lock();
        System.out.println("hello");
        lock.unlock();
    }

    public static void main(String[] args) {
        AQSDebug aqsDebug = new AQSDebug();
        new Thread(aqsDebug::sayHello, "first").start();
        new Thread(aqsDebug::sayHello, "second").start();
        new Thread(aqsDebug::sayHello, "third").start();
    }
}

为了实现三个线程相互抢占的效果,我们使用多线程断点调试的方法(参考这篇文章),让线程first先获取到锁,然后切换到线程second或者third进行调试,这样就能够看到因为获取不到锁,而加入同步队列,等待,被唤醒的过程。

切换到线程second,我们来看一下,会走到java.util.concurrent.locks.ReentrantLock.NonfairSync#lock方法,NonfairSync继承了Sync,Sync继承了AbstractQueuedSynchronizer。在lock方法中,首先尝试用CAS的方式,将AQS中用来表示同步状态的volatile变量state设置为1,这里用CAS的方式是保证原子性,因为可能有多个线程同时来争抢。

static final class NonfairSync extends Sync {
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

由于线程first已经获取了锁,所以这里会失败,然后走else,下面是代码

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

到这个方法,首先会调用尝试获取,这个方法是一个protected修饰的方法,所以会在ReentrantLock中实现(不同的类在此方法上可以自定义实现,这里不是我们AQS的重点,我们想一下独占锁,first已经获取了,那这里一定会失败),下面是代码

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;

ReentrantLock中实现tryAcquire方法的逻辑是,首先看看有没有线程已经获取了同步状态,如果有,看看已经获取的是不是当前线程,因为ReentrantLock是可重入的。这里second线程显然不是,所以返回false。

接着执行addWaiter(Node.EXCLUSIVE)

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

首先构造一个node,把当前线程封装进去。然后,由于刚开始同步队列还是空的,所以走enq方法,否则直接将构造的节点从尾部加入。这里需要注意依然是通过CAS的方式加入尾部,因为可能有多个线程一起加入同步队列。最后将节点node返回。

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

开始时队列是空的,或者并发插入尾部的时候插入失败,都会走到这里,在这里用死循环的方式尝试将node插入尾部。还有一个细节,就是当队列开始是空的的时候,会先放置一个空节点,这一点也是为了与其他情况兼容:“head指向的头结点都是当前获取锁的节点,当这个节点中的线程释放锁后会唤醒后继节点”,但是刚开始添加第一个节点时,这个节点没有获取锁,为了与其他情况兼容,在它之前插入一个空节点。当获取锁的线程执行完释放锁之后,依然会唤醒head指向节点的后继节点。

然后执行前面acquire中的acquireQueued方法,代码如下:

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)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

构造完节点,把节点加入队列中,然后需要判断当前节点的前一个节点是不是头结点(因为头结点是获取锁的线程节点,随时可能释放锁,只有后继节点有获取锁的可能,再往后面的节点不能),如果是,再次执行tryAcquire方法尝试获取锁,如果恰好获取成功,那么将自己设置为头结点,原来的头结点被GC。如果没有获取成功或者前继节点不是头结点,那么会走shouldParkAfterFailedAcquire方法,代码如下:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

看看前继节点的waitStatus是不是SIGNAL(SIGNAL表示当前继节点释放锁后会唤醒后继节点),大于0表示被取消的节点,我们的实际情况是second线程前面的节点是空节点,waitStatus=0,所以会为前继节点设置SIGNAL,来在释放锁的时候唤醒后继节点。返回false,然后由于acquireQueued方法是死循环,退出的途径就是获取到了锁。所以会继续走shouldParkAfterFailedAcquire,这一次,前继节点设置了SIGNAL,所以返回true。返回后走下面的parkAndCheckInterrupt方法,这个方法很简单,就是将当前线程阻塞,线程进入WAIT状态。

至此,线程second已经进入阻塞状态。切换到线程third开始调试,跟线程second很相似,只是不需要再在等待队列总加入空节点,直接排队到second节点的后面,然后前面节点也不是头结点,直接进入阻塞状态。

然后我们再切换到线程first,进入解锁流程。unlock会调用AQS中的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;
}

首先还是会调用tryRelease,这个方法是ReentrantLock中实现的protected修饰的方法,代码如下

protected final boolean tryRelease(int releases) {
    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;
}

ReentrantLock中实现的方法都是主要对同步状态state进行修改,解锁的时候将state减去1。返回成功后,调用unparkSuccessor,这个方法的主要作用就是唤醒后继节点。这时候second节点被唤醒成功,然后从前面讲到的parkAndCheckInterrupt方法返回,由于没在acquireQueued方法中,会再走一遍for循环,这一次能够获取锁成功,然后将前继节点从队列中移除,自己成为头结点,当自己也执行完成后,会唤醒third节点,然后third线程继续执行完成。

以上就是通过ReentrantLock独占锁来对AQS进行源码级别的剖析。

你可能感兴趣的:(Java)