JUC中AQS获取锁的方式

AbstractQueuedSynchronizer,简称AQS,是JUC中Lock机制的实现的前提,大部分锁的实现都是以这个为基础的。

看下这个类的注释:

 1 /**
 2      * Wait queue node class.
 3      *
 4      * <p>The wait queue is a variant of a "CLH" (Crai哦, Landin, and
 5      * Hagersten) lock queue. CLH locks are normally used for
 6      * spinlocks.  We instead use them for blocking synchronizers, but
 7      * use the same basic tactic of holding some of the control
 8      * information about a thread in the predecessor of its node.  A
 9      * "status" field in each node keeps track of whether a thread
10      * should block.  A node is signalled when its predecessor
11      * releases.  Each node of the queue otherwise serves as a
12      * specific-notification-style monitor holding a single waiting
13      * thread. The status field does NOT control whether threads are
14      * granted locks etc though.  A thread may try to acquire if it is
15      * first in the queue. But being first does not guarantee success;
16      * it only gives the right to contend.  So the currently released
17      * contender thread may need to rewait.
18      *
19      * <p>To enqueue into a CLH lock, you atomically splice it in as new
20      * tail. To dequeue, you just set the head field.
21      * <pre>
22      *      +------+  prev +-----+       +-----+
23      * head |      | <---- |     | <---- |     |  tail
24      *      +------+       +-----+       +-----+
25      * </pre>

底层是通过一个CLH等待队列实现的lock以及unlock

public void java.util.concurrent.locks.ReentrantLock.lock()

获取锁。

如果该锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为 1。

如果当前线程已经保持该锁,则将保持计数加 1,并且该方法立即返回。

如果该锁被另一个线程保持,则出于线程调度的目的,禁用当前线程,并且在获得锁之前,该线程将一直处于休眠状态,此时锁保持计数被设置为 1。

从上面的文档可以看出ReentrantLock是可重入锁的实现。而内部是委托 java.util.concurrent.locks.ReentrantLock.Sync.lock()实现的。 java.util.concurrent.locks.ReentrantLock.Sync是抽象类,有 java.util.concurrent.locks.ReentrantLock.FairSync和 java.util.concurrent.locks.ReentrantLock.NonfairSync两个实现,也就是常说的公平锁和不公平锁。

public final void acquire(int arg) 以独占模式获取对象,忽略中断。通过至少调用一次 tryAcquire(int)来实现此方法,并在成功时返回。否则在成功之前,一直调用 tryAcquire(int) 将线程加入队列,线程可能重复被阻塞或不被阻塞。

对公平锁而言,它的实现如下

 1 /**
 2          * Fair version of tryAcquire.  Don't grant access unless
 3          * recursive call or no waiters or is first.
 4          */
 5         protected final boolean tryAcquire(int acquires) {
 6             final Thread current = Thread.currentThread();
 7             int c = getState();
 8             if (c == 0) {
 9                 if (isFirst(current) &&
10                     compareAndSetState(0, acquires)) {
11                     setExclusiveOwnerThread(current);
12                     return true;
13                 }
14             }
15             else if (current == getExclusiveOwnerThread()) {
16                 int nextc = c + acquires;
17                 if (nextc < 0)
18                     throw new Error("Maximum lock count exceeded");
19                 setState(nextc);
20                 return true;
21             }
22             return false;
23         }

分析:

  1. 如果当前锁有其它线程持有,c != 0,进行操作2。否则,如果当前线程在AQS队列头部,则尝试将AQS状态state设为 acquires(等于1),成功后将AQS独占线程设为当前线程返回true,否则进行2。这里可以看到compareAndSetState就是使用 了CAS操作。
  2. 判断当前线程与AQS的独占线程是否相同,如果相同,那么就将当前状态位加1,修改状态位,返回true,否则进行3。这里之所以不是将当前状态位设置为1,而是修改为旧值+1呢?这是因为ReentrantLock是可重入 锁,同一个线程每持有一次就+1。
  3. 返回false。

tryAcquire失败就意味着入队列了。此时AQS的队列中节点Node就开始发挥作用了。

 1 private Node addWaiter(Node mode) {
 2         Node node = new Node(Thread.currentThread(), mode);
 3         // Try the fast path of enq; backup to full enq on failure
 4         Node pred = tail;
 5         if (pred != null) {
 6             node.prev = pred;
 7             if (compareAndSetTail(pred, node)) {
 8                 pred.next = node;
 9                 return node;
10             }
11         }
12         enq(node);
13         return node;
14     }

上面是节点入队列的一部分。当前仅当队列不为空并且将新节点插入尾部成功后直接返回新节点。否则进入enq(Node)进行操作。

 1 /**
 2      * Inserts node into queue, initializing if necessary. See picture above.
 3      * @param node the node to insert
 4      * @return node's predecessor
 5      */
 6     private Node enq(final Node node) {
 7         for (;;) {
 8             Node t = tail;
 9             if (t == null) { // Must initialize
10                 Node h = new Node(); // Dummy header
11                 h.next = node;
12                 node.prev = h;
13                 if (compareAndSetHead(h)) {
14                     tail = node;
15                     return h;
16                 }
17             }
18             else {
19                 node.prev = t;
20                 if (compareAndSetTail(t, node)) {
21                     t.next = node;
22                     return t;
23                 }
24             }
25         }
26     }

enq(Node)去队列操作实现了CHL队列的算法,如果为空就创建头结点,然后同时比较节点尾部是否是改变来决定CAS操作是否成功,当且仅当成功后才将为不节点的下一个节点指向为新节点。可以看到这里仍然是CAS操作。

acquireQueued(node,arg)

自旋请求锁,如果可能的话挂起线程,直到得到锁,返回当前线程是否中断过(如果park()过并且中断过的话有一个interrupted中断位)。

 1  /**
 2      * Acquires in exclusive uninterruptible mode for thread already in
 3      * queue. Used by condition wait methods as well as acquire.
 4      *
 5      * @param node the node
 6      * @param arg the acquire argument
 7      * @return {@code true} if interrupted while waiting
 8      */
 9     final boolean acquireQueued(final Node node, int arg) {
10         try {
11             boolean interrupted = false;
12             for (;;) {
13                 final Node p = node.predecessor();
14                 if (p == head && tryAcquire(arg)) {
15                     setHead(node);
16                     p.next = null; // help GC
17                     return interrupted;
18                 }
19                 if (shouldParkAfterFailedAcquire(p, node) &&
20                     parkAndCheckInterrupt())
21                     interrupted = true;
22             }
23         } catch (RuntimeException ex) {
24             cancelAcquire(node);
25             throw ex;
26         }
27     }

acquireQueued过程是这样的:

1.如果当前节点是AQS队列的头结点(如果第一个节点是DUMP节点也就是傀儡节点,那么第二个节点实际上就是头结点了),就尝试 在此获取锁tryAcquire(arg)。如果成功就将头结点设置为当前节点(不管第一个结点是否是DUMP节点),返回中断位。否则进行2。

2.检测当前节点是否应该park(),如果应该park()就挂起当前线程并且返回当前线程中断位。进行操作1。


总结:

AQS获取锁:创建一个独占节点到CLH队列末尾,然后自旋等待获取锁,如果前一个节点被释放,就唤醒当前节点(LockSupport unpark),使其成为继任节点,获取锁成功。获取锁成功后,会将
                     持有锁的节点设为头节点。

 

 

posted @ 2012-11-08 20:23  flying_wind  阅读(599)  评论(0编辑  收藏  举报