从ReentrantLock说到AQS

·  阅读 154

ReentrantLock

ReentrantLock的加锁和解锁

ReentrantLock是juc以api的形式提供的可重入锁,典型的使用方式是

reentrantLock.lock();
doSomeThing();
reentrantLock.unlock();复制代码

lock表示加锁,unlock表示解锁,接下来我们就看看是怎么实现的

lock

public void lock() {    
    sync.lock();
}复制代码

在ReentrantLock里面,lock有两种实现,一种是公平锁FairSync、一种是非公平锁NonfairSync

下面是NonfairSync的实现:首先使用cas修改state的值,如果修改成功,说明可以拿到锁,设置当前线程是获取到锁的线程。如果修改失败,则用排它模式获取。

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

acquire会进到aqs里的acquire方法。

这里的代码会先尝试获取锁(tryAcquire),如果获取失败,则将当前线程加到队列里(addWaiter),并将当前线程挂起(acquireQueued)。

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

再来看下tryAcquire的实现。

tryAcquire最终会进到NonfairSync的nonfairTryAcquire的方法。这个方法线判断当前state是否是0,如果是0,说明可以获取资源,那就用cas修改state的值,如果修改成功,则说明当前线程获取到了锁,设置当前线程为获取锁的线程。如果state不是0,说明已经被枷锁,就判断当前线程是不是获取锁的线程,如果是,就加获取数量加1。如果没有获取到锁,或者获取锁的线程不是当前线程,则返回false。

final boolean nonfairTryAcquire(int acquires) {
    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;
}复制代码

上面我们说过Sync在ReentrantLock里有2种实现,公平和非公平其实就体现在获取资源方面

protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            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;
    }
}复制代码

nofairTryAcquire和tryAcquire的区别在于,如果新加的线程发现当前资源可以获取时,非公平实现方式直接尝试获取,公平实现方式会线判断当前队列是否有等待的线程,如果没有才获取。

addWait方法会将当前线程加入到队列里面。

private Node addWaiter(Node mode) {
    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;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node); 
   return node;
}复制代码

然后就是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);
    }
}复制代码

shouldParkAfterFailedAcquire判断当前节点前面节点的状态,

  • 如果是SIGNAL,就说明当前节点可以park,前面节点会unpark当前节点的线程
  • 如果是>0,说明前面节点已经取消,是无用节点,删除这些节点,当前节点不能park,因为可能没有节点unpark当前节点的线程
  • 其它情况下,将前面节点置为SIGNAL,当前节点不能park,因为可能没有节点unpark当前节点的线程

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;
}复制代码

unlock

首先是unlock方法,调用sync的release方法,释放1个资源

public void unlock() {
    sync.release(1);
}复制代码

在release方法,先tryRelease释放资源,判断当前线程是不是获取资源的线程,如果不是就报错,如果是就将state的值减去releases的值,如果减去之后=0,就说明资源被全部释放,当前线程不再占有锁了。这时会调用unparkSuccessor将后置节点unpark。


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

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;
}复制代码

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;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}复制代码

什么是AQS

AQS全称AbstractQueuedSynchronizer,juc里很多工具类都是基于这个实现的。从上面我们可以看出,AQS提供了获取资源、释放资源的能力,如果获取资源成功,当前线程就会继续执行,如果获取资源失败,当前线程就会被挂起(park)。

AQS维护了1个队列,用于存储那些没能获取资源而被挂起的线程。队列里的每个节点都用AQS的内部类Node表示。

Node

Node是同步队列的节点,封装了对应的线程和节点在队列的状态。

队列里的每个节点都有状态:

  • CANCELLED,表示当前节点被取消,当前节点表示的线程不会再争夺锁
  • SIGNAL,表示当前节点后面节点需要unpark,我们在前面看到shouldParkAfterFailedAcquire如果发现前面节点是SIGNAL状态,就表示自己肯定会被unpark,所以返回true表示可以park当前线程
  • CONDITION,表示当前线程正在等待一个condition,condition用到,后面讲
  • PROPAGATE,表示下一个acquireShared应该无条件传播
  • 0,默认状态

Condition

Condition有await、singal、singalAll方法,对应Object的wait、notify、notifyAll。

Condition也维护了1个队列,表示在当前这个Condition对象上等待的线程。

await方法现在队列里追加一个等待着节点,然后把当前线程获取的资源全部释放,并记录当前线程释放的资源数,后面重新获取锁的时候要用。然后判断当前节点是否在同步队列,这里指的是Node维护的节点,不是Condition维护的节点。如果不在同步队列,则挂起当前线程。当线程从挂起状态恢复后,当前节点已经在同步队列了,这时会尝试获取资源。获取资源之后,把取消的等待着清理掉。

public final void await() throws InterruptedException {
    if (Thread.interrupted())        throw new InterruptedException();
    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;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();//清理取消的等待着
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}复制代码

signal方法获取第1个等待着,把第1个等待者唤醒。

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}复制代码

private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}复制代码

transferForSignal方法,修改节点的状态,然后把节点置入同步队列,前段await流程里面会判断是否在同步队列里面,就是这个时候置入同步队列的。

final boolean transferForSignal(Node node) {
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;}复制代码

总结

最开始看AQS没有结合使用场景,不太好理解。从ReentrantLock入手,看AQS如何控制线程的阻塞和唤醒,就比较好理解了。然后引入了AQS的节点的实现、AQS的Condition的实现。

最后还有一个点前面没说道,我们经常看到park、unpark操作,有什么作用呢。park会将当前线程挂起。unpark会将指定线程唤醒,如果指定线程还没有挂起,那么指定线程挂起(park)的事后会直接返回,不会挂起。


分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改