AQS Series (1) ---- ReentrantLock Africa fair locks lock and unlock

In fact, I have always wanted to write aqs (AbstractQueuedSynchronizer), but found that this class features a little wide, the design concept is relatively large.

Possible with my ability should be the first to write this aqs class jdk applied to, and then come back to re-organize aqs is more reasonable ideas.

The most commonly used and should be the class of ReentrantLock direct (re-entry lock) a.

To understand this requires a basic understanding of some concepts aqs: synchronous queue and node status bits.

Here we analyze two core methods NonfairSync the lock and unlock, which will be much simpler than lock unlock.

Take a look at the class hierarchy NonfairSync

We can see unfair lock NonfairSync indirectly inherited aqs

ReentrantLock simply be seen realized the Lock interface which there Sync, NonfairSync, FairSync as an internal class, can be understood as a kind of package.

Lock

By looking at the comment entry method ReentrantLock to preheat it:

/**
 * 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. If the current lock is not held by another thread can acquire the lock and then immediately return immediately, then the count is set to 1;
  2. If the current thread already holds the lock, then count + 1, and the method returns immediately;
  3. If the current lock is held by another thread, and this thread will be disabled for thread scheduling, and maintaining sleep until it can acquire the lock when the thread can acquire the lock, the counter is set to 1. (In this case the method blocks)

Look directly into the point NonfairSync all lock method.

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

And notes as described, try this method in place if the CAS direct violence to acquire the lock state, successful owner then set to the current thread is over.

Then look else after the failure,

/**
 * 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();
}

Note that this method is a method of aqs, that this should be a general method (because the lock can be achieved through inheritance aqs)!

Look at tryAcquire (arg), note that this method is a template method is referred to subclass implementation of

    /**
     * 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;
    }

We found a realization in 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的值的变化。

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

Guess you like

Origin www.cnblogs.com/kobebyrant/p/reentrantLock.html