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