本文主要来分析一下AQS共享模式锁的获取和释放,AQS其实只是一个框架,它主要提供了一个int类型的state字段,子类继承时用于存储子类的状态,并且提供了一个等待队列以及维护等待队列的方法。至于如何使用这个状态值和等待队列,就需要子类根据自己的需求来实现了。
以Semaphore类为例,Semaphore允许多个线程同时获得信号量先来看一下Semaphore的接口:
//Semaphore public void acquire() throws InterruptedException { sync.acquireSharedInterruptibly(1); }
同样的,sync是一个定义在Semaphore中的AQS的抽象子类,在Semaphore类中有两种实现,一个是公平的,一个是非公平的。转到AQS中的 acquireSharedInterruptibly
方法,
//AbstractQueuedSynchornizer public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); //由于本文分析共享模式锁,所以说tryAcquireShared尝试获取的是permit而不是锁 //tryAcquireShared尝试获取相应数量的permit,如果失败返回负值。返回0代表获取成功但是下次调用会失败,返回正值代表获取成功而且下次调用可能也会成功 //可以理解为返回0代表只有0个permit,所以下次调用会失败,而返回正值代表还有permit,所以下次调用可能会成功 if (tryAcquireShared(arg) < 0) //获取失败后需要新建一个等待节点并将节点加入等待队列 doAcquireSharedInterruptibly(arg); } private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { //新建一个共享模式的节点并将其加入等待队列 final Node node = addWaiter(Node.SHARED); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head) { //如果其前驱节点是头节点,那么再次尝试获取permit int r = tryAcquireShared(arg); if (r >= 0) { //如果获取成功那么将该节点设置成头节点,并且如果r>0,代表还有剩余的permit,所以如果该节点的后继节点也是共享模式的,就把后继节点也唤醒 setHeadAndPropagate(node, r); p.next = null; // help GC failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }
来看一下 setHeadAndPropagate
方法,这个方法和 setHead
不同的地方在于它不仅设置了等待队列的头节点,并且检查其后继节点是否 可能
是共享模式节点,如果是,而且传入的 propagate
大于0或者头节点设置了 PROPAGATE
状态,那么需要调用 doReleaseShared
方法来唤醒后继节点。 setHeadAndPropagate
方法的处理过程比较保守,可能会导致很多不必要的唤醒。
private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // Record old head for check below setHead(node); //如果propagate>0,代表有剩余的permit,唤醒共享模式节点 //如果h.waitStatus = PROPAGATE,表示之前的某次调用暗示了permit有剩余,所以需要唤醒共享模式节点 //由于PROPAGATE状态可能转化为SIGNAL状态,所以直接使用h.waitStatus < 0来判断 //如果现在的头节点的waitStatus<0,唤醒 //如果现在的头节点等于null,唤醒 if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; //如果后继节点为null,whatever唤醒 if (s == null || s.isShared()) doReleaseShared(); } }
可以看到 setHeadAndPropagate
方法的原则是宁滥勿缺,反正 doReleaseShared
方法会继续后来的处理:
private void doReleaseShared() { for (;;) { Node h = head; //如果头节点不为空且头节点不等于尾节点,亦即等待队列中有线程在等待 //需要注意的是,等待队列的头节点是已经获得了锁的线程,所以如果等待队列中只有一个节点,那就说明没有线程阻塞在这个等待队列上 if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { //如果头节点的状态是SIGNAL,代表需要唤醒后面的线程(SIGNAL状态可以看做是后继节点处于被阻塞中) if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases //唤醒后继节点 unparkSuccessor(h); } //如果头节点的状态为0,说明后继节点还没有被阻塞,不需要立即唤醒 //把头节点的状态设置成PROPAGATE,下次调用setHeadAndPropagate的时候前任头节点的状态就会是PROPAGATE,就会继续调用doReleaseShared方法把唤醒“传播”下去 else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } //如果头节点被修改了那么继续循环下去 if (h == head) // loop if head changed break; } }
根据自己的思考总结一下,不保证正确性:
SIGNAL
状态,所以 SIGNAL
状态代表其后继节点正在阻塞中。 PROPAGATE
状态代表唤醒的行为需要传播下去,当头节点的后继节点并未处于阻塞状态时(可能是刚调用 addWaiter
方法添加到队列中还未来得及阻塞),就给头节点设置这个标记,表示下次调用 setHeadAndPropagate
函数时会把这个唤醒行为传递下去。 PROPAGATE
状态的意义主要在于,每次释放permit都会调用 doReleaseShared
函数,而该函数每次只唤醒等待队列的第一个等待节点。所以在本次归还的permit足够多的情况下,如果仅仅依靠释放锁之后的一次 doReleaseShared
函数调用,可能会导致明明有permit但是有些线程仍然阻塞的情况。所以在每个线程获取到permit之后,会根据剩余的permit来决定是否把唤醒传播下去。但不保证被唤醒的线程一定能获得permit。