AQS共享模式解析
- 共享锁,需要实现tryAcquireShared和tryReleaseShared方法,但不管是共享还是排他锁都继承自AQS,所以其节点内部结构不会有变化。
- 首先我们要了解什么是共享锁,就是当某个线程对临界区进行读取时其他线程只能在对临界区进行读操作,不能有排他锁的存在。直到线程释放共享锁后写操作才能尝试排他锁,Semaphore是共享模式的实现类,根据AQS子类实现列表我们能看出Semaphore也是内部有一个Sync类,并有FairSync和NonfairSync类继承自Sync类,所以Semaphore也可以实现公平与非公平。
- 实现过程:
- 那我们先来看看Sync做了什么:
首先Sync类也是自己重写了AQS的tryReleaseShared方法,并且又自定义了方法nonfairTryAcquireShared,跟ReentrantLock的排他模式实现一样,nonfairTryAcquireShared用于非公平模式时获取锁,而tryReleaseShared则是公平、非公平模式时释放共享锁,那我们还是从加锁开始:abstract static class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = 1192457210091910933L; Sync(int permits) { setState(permits); } final int getPermits() { return getState(); } final int nonfairTryAcquireShared(int acquires) { for (;;) { int available = getState(); int remaining = available - acquires; if (remaining < 0 || compareAndSetState(available, remaining)) return remaining; } } protected final boolean tryReleaseShared(int releases) { for (;;) { int current = getState(); int next = current + releases; if (next < current) // overflow throw new Error("Maximum permit count exceeded"); if (compareAndSetState(current, next)) return true; } } final void reducePermits(int reductions) { for (;;) { int current = getState(); int next = current - reductions; if (next > current) // underflow throw new Error("Permit count underflow"); if (compareAndSetState(current, next)) return; } } final int drainPermits() { for (;;) { //获取当前加锁数 int current = getState(); //当锁的计数值等于0或者通过cas操作将计数值置为0则返回当前计数值 if (current == 0 || compareAndSetState(current, 0)) return current; } } }
1.acquire() 开始获取共享锁
不管是公平模式、非公平模式都是从该方法开始加锁,之后进入AQS的acquireSharedInterruptibly方法内部:
进入acquireSharedInterruptibly方法后判断当前线程是否被中断,然后通过你选择的是初始化的是公平锁还是非公平锁进入不同的方法调用。那我们剑指tryAcquireShared方法- 非公平模式
看了上面的调用看到非公平锁直接会调用Sync类自定义的nonfairTryAcquireShared方,那其中做了什么操作:
我们看到进入nonfairTryAcquireShared方法后自旋的尝试获取共享锁,我们首先看到会首先获取state也就是当前共享锁的个数,为什么会减去当前需要获取锁的数量,这里大家看了排他锁的话,可能会产生些许疑问,那我们这里需要转变一下思维,首先在共享锁创建之时需要传入临界区最大线程访问数量,那就是因为这个传入的数值造成这个原因咯,直接用源代码来看吧:final int nonfairTryAcquireShared(int acquires) { for (;;) { int available = getState(); int remaining = available - acquires; if (remaining < 0 || compareAndSetState(available, remaining)) return remaining; } }
这里我们就直接看非公平锁的创建,二者是一样的
看到这里非公平锁会调用父类的构造器也就是调用Sync的构造:
看到这里大家就应该知道了,初始化时会将这个共享锁的最大值赋值给state.我们再来推nonfairTryAcquireShared方法中的逻辑,也就我每获取到一把共享锁,state值就会减去1,这样当state最后值为负数的时候就证明没有锁可获取了否则就将state的值进行更新。
接着我们看到acquireSharedInterruptibly方法中,当state减去当前获取锁的数量为负数时就会执行doAcquireSharedInterruptibly方法,我们进入doAcquireSharedInterruptibly查看该方法的实现逻辑:
我们看到doAcquireSharedInterruptibly首先将节点加入队列中,如果当前节点是队列第二个节点则尝试获取锁,如果成功需要调用setHeadAndPropagate方法: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) { /** * 如果进入判断说明当前节点是第二节点,因为头节点随时可能 * 会释放锁,所以需要第二个节点自旋的尝试获取锁 */ int r = tryAcquireShared(arg); //如果剩余共享锁数量是大于等于0的说明当前线程获取锁成功 if (r >= 0) { //将当前节点标记为头节点并唤醒下一个非取消节点进行自旋获取锁 setHeadAndPropagate(node, r); //将头节点的下一个节点引用更为为null,方便GC进行回收 p.next = null; // help GC //设置failed为false,则不从队列中删除该节点 failed = false; return; } } /** * 当前节点非第二个节点则,获取上一节点P的等待状态: * * a.P的状态为SIGNAL则将当前线程阻塞。 * * b.P的状态为取消状态,则往上追溯找到P节点的上一节点, * 查看是否为取消状态,如果是则再往上追溯,直到找到非 * 取消节点或是遍历完整个队列,则最后一个就是头节点,将 * 当前节点的prev指向该节点和将该节点的next指向当前节点 * 然后重新执行循环体,看能否尝试获取锁。 * * c.P的状态既不是SIGNAL也不是取消,则通过cas操作将 * 当前节点状态设置为SIGNAL,然后重新执行循环体,看能 * 否尝试获取锁 */ if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }
private void setHeadAndPropagate(Node node, int propagate) { //拿到当前的头节点 Node h = head; //把当前节点标记为头节点 setHead(node); /** * 进入判断条件: * a.锁剩共享锁数量大于0 * b.前头节点为null * c.前头节点等待状态小于0 * d.当前节点为nul * e.当前节点等待状态小于0 */ if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { //获取当前节点的下一节点 Node s = node.next; //如果下一节点为null或者下一节点为共享模式 if (s == null || s.isShared()) //唤醒队列中的离头节点最近的非取消节点 doReleaseShared(); } }
所以说 setHeadAndPropagate方法的主要将当前节点设置为头节点,并在资源充足的情况下对最近的非取消节点进行唤醒。private void doReleaseShared() { for (;;) { //获取头节点 Node h = head; //如果头节点不为null,且不能与尾节点相等,也就是说队列中至少有两个节点 if (h != null && h != tail) { //获取头节点等待状态 int ws = h.waitStatus; //如果头节点状态为SIGNAL if (ws == Node.SIGNAL) { //将头节点的等待状态设置为0,如果不成功则循环到设置成功为止 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases /** * 跟排他锁中调用的相同的方法,从列表中获取一个继头节点之后的第一个非取消的节点 * ,然后对其进行唤醒操作,让其进入自旋等待获取锁 */ unparkSuccessor(h); } /** * 如果头节点状态为0就设置头节点状态为PROPAGATE, * 也就是设置为共享模式让其具有唤醒下节点的功能,设置失败则循环至成功为止 */ else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } //以上操作完成后结束循环 if (h == head) // loop if head changed break; } }
小结:我们总结一下非公平模式下的获取共享锁的过程,首先尝试去获取锁,如果没有位置了,则将当前线程加入到同步队列中,之后判断一下当前队列是不是同步队列中的第二个节点,如果是的话就尝试的去获取锁,如果获取成功则将当前节点设置为头节点,并且找到下一个非取消节点进行唤醒,让其自旋的去获取锁。 - 公平模式
上面我们说了公平模式和非公平模式进入的方法都是一样的只是调用的是自己的,那我们直接看公平锁实现的tryAcquireShared是怎么样的:
公平模式跟非公平模式不同的就是关于hasQueuedPredecessors的一个返回值判断了,那这个hasQueuedPredecessors又是什么呢protected int tryAcquireShared(int acquires) { for (;;) { if (hasQueuedPredecessors()) return -1; int available = getState(); int remaining = available - acquires; if (remaining < 0 || compareAndSetState(available, remaining)) return remaining; } }
说的直白一点其实hasQueuedPredecessors就是判断同步队列中是否有队列存在,如果存在判断当前节点是不是第二个节点如果是才允许其去尝试获取锁,这样每次都需要判断同步队列中是否有节点存在,就不会出现后节点先获取锁的情况。public final boolean hasQueuedPredecessors() { Node t = tail; Node h = head; Node s; /** * 进入方法后新获取队列中的头节点和尾节点 * * a. 如果头节点等于尾节点,那说明要不都为null或者其指向的是一个节点,则可以让当前节点去尝试获取锁。 * * b. 如果不相等,说明队列中至少有两个节点以上的节点,此时为什么还要做其他判断呢,因为队列头节点 * 是获取锁对象并且随时可能随时释放锁的节点,之前我们讲过等待状态,当头节点获取锁对象后就会唤醒 * 其下一节点不停的自旋获取锁,那后面的判断就是为了判断当前节点是否时第二个节点了。 * * c. 首先判断第二个节点是否为null,为null说明此时另外一个线程正在进行第一次入队操作,入队时会先更 * head=new Node(),而tail还未进行设置,所以二者不相等,而head又没有下节点所以为null,所以当前线程 * 就不能去获取锁了,并将指向更改。 * * d. 那如果不为null,则判断当前线程和第二个线程是否为同一线程,如果是则可以尝试获取锁对象。 */ return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }
- 非公平模式
- release() 共享锁的释放
释放锁时对非公平和公平没有影响的所以二者之间不会有什么区别,我们先看看调用过程
我们看到release方法会直接调用releaseShared方法,这个方法是AQS自带的方法,我们看看方法内部
进入方法后其先调用了tryReleaseShared方法,也是共享锁的实现方法之一,之前我们介绍Sync类时,就说过Sync直接重写了该方法,那我们看看该方法做了些什么
首先也是获取当前还剩余共享锁的个数,之后与需要释放的锁相加还比以前小说明当前程序出现问题,否则将state重置protected final boolean tryReleaseShared(int releases) { for (;;) { int current = getState(); int next = current + releases; if (next < current) // overflow throw new Error("Maximum permit count exceeded"); if (compareAndSetState(current, next)) return true; } }
返回到上层方法releaseShared中,如果锁释放成功则会调用doReleaseShared方法,关于这个方法我们上面已经介绍过了就是判断头节点是否存在并且队列中的节点必须大于等于2,此时会判断头节点的状态,如果为SIGNAL,则将其状态设置为0并唤醒其下一节点;如果其状态为0,则设置为共享模式,让其也具有唤醒下节点的功能。
- 那我们先来看看Sync做了什么: