AQS究竟在抽象什么?AQS实现一个并发工具究竟还需要实现什么?

23 阅读4分钟

当我们在使用一个并发工具时,总是围绕着三个最重要的入口方法来使用的:构造方法、 acquire 获取方法、release 释放方法

  1. 构造方法在抽象什么呢? 细看三个实现 AQS 的并发工具: ReentrantLockCountDownLatchSemaphore 。它们的构造函数都是在做一件事,围绕构造一个同步器实例,存放于属性内部。该同步器就是对 AQS 的具体实现,不过它们三者都有两种实现:公平非公平之分。

那在构造对应的同步器实例的时候, 需要用到 AQS 提供的什么功能呢?

// CountDownLatch的同步器构造函数
Sync(int count) {  
    setState(count);  
}
// Semaphore的同步器构造函数实现
Sync(int permits) {  
    setState(permits);  
}

从上面可以看出, ReentrantLock 直接用的就是无参构造,没有做任何操作,而另外二者都是在初始化对应的 state 状态。从而可以引出 AQS 中最重要的一个属性之一:state 表示临界资源的状态,可以根据独占式和共享式来表明不同的状态。比如:CountDownLatch 中表明对应的计数,ReentrantLock 中表示加锁的状态,比如重入次数,Semaphore 表示共享资源还可以被几个线程同时持有。接下来让我们把视角移到具体的使用。 2. acquire 方法在抽象什么?我们需要实现什么?

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

从上面的代码我们可以看出,这里一共干了两件事:尝试获取锁,加入等待队列。这里的 tryAcquire就是并发容器中需要去具体实现的方法,用于对 state 进行操作并返回结果,这里 AQS 只是抽象了其逻辑,保证了执行的顺序,从而我们使用者只需要关注对应的实现逻辑即可,具体的执行流程都由 AQS 抽象出来了。 紧接着是 AQS 中具体的实现 addWaiteracquireQueued,用于获取临界资源失败后到等待队列中等待的操作,而不是一直尝试自旋争抢。这里具体的实现就是围绕创建 Node 节点和将 Node 节点入队等待合适的时机由前面的 Node 对其进行唤醒。

那共享式有什么区别呢?

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

第一个操作同样也是获取共享资源并更改对应的 state ,而这里的失败与否不再是互斥关系,而可以存在多个成功的可能,失败了就直接进入等待队列排队。所以整体上并无明显区别 2. 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;  
}

这里的 release 实现也是同 acquire 一样,抽象了方法的流程,具体的实现交由具体的工具进行实现。而 tryRelease 就是工具需要实现的第二个方法,在独占式中,如果锁得到了释放,那么就会通过等待队列的头节点来对下一个节点进行 unpark 唤醒,并且会涉及到下一个节点的来继续作为头节点

public final boolean releaseShared(int arg) {  
    if (tryReleaseShared(arg)) {  
        doReleaseShared();  
        return true;  
    }    
    return false;  
}

共享式和独占式几乎一致,并没有太大的差别,只需注意在共享模式下共享资源可以同时被多个线程持有,那么在等待队列中如何实现一个唤醒链呢?用 Node.PROPAGATE 来保证唤醒操作的传播成功,若节点进入了对应的唤醒逻辑,但是存在竞争被另一个线程争抢了唤醒机会,那么就需要该 Node.PROPAGATE 保证传播的有效性,防止消息丢失。

private void doReleaseShared() {  
    /*  
     * Ensure that a release propagates, even if there are other     
     * in-progress acquires/releases.  This proceeds in the usual     
     * way of trying to unparkSuccessor of head if it needs     
     * signal. But if it does not, status is set to PROPAGATE to     
     * ensure that upon release, propagation continues.     
     * Additionally, we must loop in case a new node is added     
     * while we are doing this. Also, unlike other uses of     
     * unparkSuccessor, we need to know if CAS to reset status     
     * fails, if so rechecking.     
     */    
    for (;;) {  
        Node h = head;  
        if (h != null && h != tail) {  
            int ws = h.waitStatus;  
            // 同时有两个线程进入该处,但是一定只有一个线程成功,那么另一个线程就会失败进入到下一个循环
            if (ws == Node.SIGNAL) {  
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))  
                    continue;            // loop to recheck cases  
                unparkSuccessor(h);  
            }            
            // 下一次进入循环就会在此处判断
            else if (ws == 0 &&  
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))  
                continue;                // loop on failed CAS  
        }  
        if (h == head)                   // loop if head changed  
            break;  
    }
}