AQS系列(1)----ReentrantLock中非公平锁的lock和unlock

其实一直想专门写aqs(AbstractQueuedSynchronizer),但是发现这个类功能有点广泛,设计理念更是比较庞大。

可能以我的能力应该是先写jdk中应用到这个aqs的类,然后再重新回过头来整理aqs才是比较合理的思路。

而其中最常用而且直接的类应该就是ReentrantLock(重入锁)了。

要看懂这个需要基本了解aqs的一些概念:同步队列以及节点状态位。

这里我们只分析两个核心方法NonfairSync的lock和unlock,其中lock比unlock会简单很多。

先看看NonfairSync的类层次

可以看到非公平锁NonfairSync是间接继承了aqs

可以看到ReentrantLock单纯地实现了Lock接口,里面又有Sync,NonfairSync,FairSync作为内部类,可以理解为一个包装类了。

Lock

通过看ReentrantLock的入口方法的注释来预热一下:

/**
 * Acquires the lock.
 *
 * <p>Acquires the lock if it is not held by another thread and returns
 * immediately, setting the lock hold count to one.
 *
 * <p>If the current thread already holds the lock then the hold
 * count is incremented by one and the method returns immediately.
 *
 * <p>If the lock is held by another thread then the
 * current thread becomes disabled for thread scheduling
 * purposes and lies dormant until the lock has been acquired,
 * at which time the lock hold count is set to one.
 */
public void lock() {
    sync.lock();
}
  1. 如果当前锁没有被其他线程持有则马上可以获取锁并立刻返回,然后将计数设置为1;
  2. 如果当前的线程已经持有了锁,那计数+1,且方法立刻返回;
  3. 如果当前的锁被其他线程持有,然后这个线程则会被线程调度所禁用,并且维持休眠直到能够获取锁,当这个线程能够获取锁的时候,计数器设为1。(这个情况该方法会阻塞)

直接点进去看NonfairSync 都lock方法。

    /**
     * Performs lock.  Try immediate barge, backing up to normal
     * acquire on failure.
     */
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

和注释描述的一样,这个方法在if的地方直接尝试暴力CAS来获取锁状态,成功的话当前线程置为owner然后就结束了。

然后看失败后的else,

/**
 * Acquires in exclusive mode, ignoring interrupts.  Implemented
 * by invoking at least once {@link #tryAcquire},
 * returning on success.  Otherwise the thread is queued, possibly
 * repeatedly blocking and unblocking, invoking {@link
 * #tryAcquire} until success.  This method can be used
 * to implement method {@link Lock#lock}.
 *
 * @param arg the acquire argument.  This value is conveyed to
 *        {@link #tryAcquire} but is otherwise uninterpreted and
 *        can represent anything you like.
 */
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

注意这个方法是aqs的方法了,也就是说这个应该是一个通用方法(因为锁可以通过继承aqs来实现)!

先看tryAcquire(arg),注意这个方法是一个模板方法,是交由子类实现的

    /**
     * Performs non-fair tryLock.  tryAcquire is implemented in
     * subclasses, but both need nonfair try for trylock method.
     */
    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;
    }

我们在ReentrantLock找到了实现。

这里面的思想就是非公平获取的思想:如果当前无锁则直接去暴力CAS抢锁(不管那些在aqs同步队列里面等了大半天的线程节点),或者看看当前线程是不是持有了锁,那这里就可以单线程操作将计数器加一就好了(因此叫做重入锁)。

如果是抢到了锁,或者当前线程已经持有了锁了,那就结束了完事了。

如果还是没拿到锁,至此当前的线程已经两次cas抢锁失败了,是时候要用极端办法了。

回看acquire方法

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

tryAcquire返回false的话下一步就是看addWaiter(Node.EXCLUSIVE)方法了。

/**
 * Creates and enqueues node for current thread and given mode.
 *
 * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
 * @return the new node
 */
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;
}

先以该线程创建一个独占锁节点作为线程节点,然后队列非空的话尝试cas加入等待队列的队尾;

先不看return 因为不管这里这么样return都是返回当前线程节点的。

先看cas加入队尾失败的情况(cas失败或者队列为空)进入enq方法

/**
 * Inserts node into queue, initializing if necessary. See picture above.
 * @param node the node to insert
 * @return node's predecessor
 */
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和tail。然后就是不停地自旋cas尝试让当前线程节点加入同步队列的尾端。

意思已经很明确了,这个线程必须一定要进同步队列!注意这里是return t,node的前驱节点,但是调用它的addWaiter方法并没有取他的返回值!我之前就是看这里被enq返回值误导了很久。

继续看acquire方法

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

进去了acquireQueued方法,获取队列,

/**
 * Acquires in exclusive uninterruptible mode for thread already in
 * queue. Used by condition wait methods as well as acquire.
 *
 * @param node the node
 * @param arg the acquire argument
 * @return {@code true} if interrupted while waiting
 */
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);
    }
}

这里的入参是当前的线程节点node,而且这个方法是不可中断的方法。

这个方法就是再给刚刚入队的方法一个交代,看看他到底应该怎么往下走。

这里同样是一个自旋操作,但是一般情况下这个方法不会像前一个入队方法一样无节制地自旋,无论如何都要入队那种。

如果他是头结点,那么他就要去再试试能不能抢到锁,不行的话就要看是不是应该park休息一下。

/**
 * Checks and updates status for a node that failed to acquire.
 * Returns true if thread should block. This is the main signal
 * control in all acquire loops.  Requires that pred == node.prev.
 *
 * @param pred node's predecessor holding status
 * @param node the node
 * @return {@code true} if thread should block
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    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.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

这个方法我一开始看真的很抽象。这里我们要先思考这个方法应该怎么实现,这个方法传参是当前线程节点及其前驱,方法名叫做如果取锁失败是否应该休息,那什么时候应该休息呢?你前面如果有很多排队取锁的线程,而且他们个个很生猛的时候,是不是就不要去凑热闹了?

这个方法正是这个意思,先看前一个节点是不是signal状态,是的话就返回true,这时候就不自旋了,可以休息了。

如果前驱的状态>0那就是cancelled了的,坑爹货,那要一直循环看他前面还有没有坑货,直到找到一个不是很坑的货(或者是head节点),重塑节点连接关系。

如果前驱的状态<0且不是singal,那就让他成为signal(这里涉及到aqs知识,请看aqs状态解释),返回false,出去继续自旋。

/**
 * Convenience method to park and then check if interrupted
 *
 * @return {@code true} if interrupted
 */
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

上述方法返回true会进入该方法,意思是休眠而且返回当前节点的中断状态以及清空中断状态。

现在有两个小问题:

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);
    }
}

这个方法中为什么这么关注中断状态?

什么时候会能进入到cancelAcquire呢?

我说说我的理解

对于第一个问题:

acquireQueued 方法体明确说了是uninterruptible的,但是线程如果在执行过程中被其他线程提示中断了怎么办呢?那总不能丢失掉中断状态吧,那只能将中断状态保存起来,返回给上层,如果被中断了,然后再在上面的acquire方法调用selfInterrupt,将中断位保存住。核心就是要保证被中断的话中断信息不丢失。

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

第二个问题:

如果但看非公平锁的实现是只有在tryAcquire方法中抛异常才能进入这个cancelAcquire,但是这个异常又是锁重入次数超限才能发生的,是只有这种情况了嘛,那也太少几率发生了吧。事实上应该是由于tryAcquire是一个模板方法,是可以给予其他框架和个人来实现的一个方法,方法体里面可以允许自由地抛异常,那么在这种时候,就可以进入cancelAcquire来做一些清理工作了。

这个cancelAcquire方法比较难,看了好几遍都没看懂,以后有机会再来补充了。

这里我们总结一下一个线程调用lock的流程:

  1. 先会有两次尝试cas取锁的机会
  2. 都失败的话有一次cas入aqs同步队列尾的机会
  3. 再失败的话自旋进入同步队列尾端
  4. 成功入队列之后看情况下一步怎么走:万一能成为队列头 则继续cas尝试获取锁,否则找机会休息一波再战。

unlock

/**
 * Attempts to release this lock.
 *
 * <p>If the current thread is the holder of this lock then the hold
 * count is decremented.  If the hold count is now zero then the lock
 * is released.  If the current thread is not the holder of this
 * lock then {@link IllegalMonitorStateException} is thrown.
 *
 * @throws IllegalMonitorStateException if the current thread does not
 *         hold this lock
 */
public void unlock() {
    sync.release(1);
}

注意的是,如果该线程没持有该锁,则会抛异常。

实现是在aqs里面的:

/**
 * Releases in exclusive mode.  Implemented by unblocking one or
 * more threads if {@link #tryRelease} returns true.
 * This method can be used to implement method {@link Lock#unlock}.
 *
 * @param arg the release argument.  This value is conveyed to
 *        {@link #tryRelease} but is otherwise uninterpreted and
 *        can represent anything you like.
 * @return the value returned from {@link #tryRelease}
 */
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 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;
    }

回看release,如果是这个线程进入了这个锁不止一次,那就是会返回false;

如果不是的话,那就是进入unparkSuccessor方法来对头部方法解锁;

/**
 * Wakes up node's successor, if one exists.
 *
 * @param node the node
 */
private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    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);
}

这个方法的核心思想就是找一个节点唤醒。

一开始会尝试头结点指向如果是坑爹货,那就放弃他了,直接从尾端开始找,找到第一个就唤醒它。

唤醒之后做什么呢?

那就是从acquireQueued方法中醒来,继续自旋看看自己是不是头节点了从而找机会抢锁出队列了。

这里有个疑问没解决:

为什么这个unparkSuccessor方法这么大胆直接将头的下一个节点置空?然后从尾端开始往前找第一个waitstatus成立的节点唤醒呢?为什么不是找最前一个呢?

个人的猜测是在ReentrantLock中,不会发现head指向的下一个节点的是null或者cancelled的情况,for循环是不会进入的。

要解答这个问题,需要明白的是头节点的连接什么时候能够得到重置,还有就是线程节点的waitStatus的值的变化。

解锁的方法比较简单,也没什么可以解决的,如果谁能解答最后这个疑问,麻烦能留言告诉我一下,谢谢!

posted @ 2019-07-31 23:35  misslengleng  阅读(502)  评论(0编辑  收藏  举报