深入理解AQS(二)- 共享模式

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解AQS(二)- 共享模式相关的知识,希望对你有一定的参考价值。

参考技术A 独占锁被某个线程持有时,其他线程只能等待当前线程释放后才能去竞争锁,而且只有一个线程能竞争锁成功。

共享锁是可以被共享的,它可以被多个线程同时持有。如果一个线程获取共享锁成功,那么其他等待的线程也会去获取共享锁,而且获取大概率会成功。共享锁典型的有ReadWriteLock、CountdownLatch。

共享锁的acquireShared

独占锁的acquire

这边共享锁的acquiredShared方法其实与独占锁的acquire方法类似,tryAcquireShared都是需要子类去实现,区别是独占锁的tryAcquire返回的是boolean,而共享锁的tryAcquired方法返回的是int:

这边可以看下CountDownLatch中tryAcquiredShared的实现

state在AQS中是一个状态标识,具体的含义可以由子类来定义,在这边state定义成了数量。state等于0,则说明CountDownLatch的计数器为0,返回1成功,否则返回-1失败。

这部分逻辑和独占锁的addWaiter和acquireQueued的逻辑大体相同,只是把独占锁的逻辑addWaiter包括selfInterrupt都移到了一个方法中。
区别有:

shouldParkAfterFailedAcquire,parkAndCheckInterrupt等方法之前在独占锁中已经有过介绍了,这边就不再一一赘述了。

setHeadAndPropagate方法主要是为了设置获取锁的节点为头节点,并接下去直接唤醒后继节点。

这边if (s == null || s.isShared()) 为何s==null也要继续唤醒后继节点呢?我的理解是这样子的:

addWaiter中添加新节点到队列尾部,但是设置2个指针的指向时并不是原子操作。极端情况下,在判断s==null的时候新加入的节点只prev指针指向了前继节点,而前继节点的next指针还没有指向新加入的节点,所以node.next==null这个判断并不能说明当前节点的后继节点一定为空。isShared判断后继线程是否为共享模式,如果不是共享模式则直接跳过。当然如果node.next==null满足后出现的后继线程是独占锁,那独占锁获取是获取不到的因为锁已经被占用,只有共享模式下才能获取锁。

此时新加入的节点并没有执行park被挂起,当进入doReleaseShared后调用后继节点的unPark方法,即使后面在parkAndCheckInterrupt中调用了park方法,该线程也不会被阻塞挂起,这样就可以避免一次阻塞操作。而且doReleaseShared中唤醒后继节点unparkSuccessor方法是从尾节点向前继节点进行遍历,所以就保证了即使当前节点的next指针指向为空,新加入的节点也能够被唤醒。

doReleaseShared方法到下面分析释放锁时在看。

1、releaseShared

共享锁tryReleaseShared和和独占锁tryRelease一样都需要子类去实现。

2、doReleaseShared

一个线程成功获取共享锁后会在2个地方去调用doReleaseShared方法,一个是在获取锁后设置头节点的setHeadAndPropagate方法中;另一个是在释放锁releaseShared成功后调用。

A获取锁成功后,调用doReleaseShared唤醒B,当然保证B线程是共享模式同时也能够获取共享锁。

B获取共享锁后,尝试去唤醒C,B成为新的头节点。

节省了唤醒后继节点的速度,同时可能会出现多个doReleaseShared同时在运行,这边通过cas保证多个线程唤醒一个节点时只有一个线程能成功。

如果头节点的状态为Node.SIGNAL则说明后继节点是需要被唤醒的;

这边这个判断其实是一种极端状态,在最外层if (h != null && h != tail)已经进行了一次判断,保证当前队列至少有2个节点,但是这边2个节点的状态可能出现这样一种情况:

newHead刚成为头节点,而tail节点刚进入成为尾节点,但tail节点并未执行shouldParkAfterFailedAcquire把前继节点修改为SIGNAL,满足!compareAndSetWaitStatus(h, 0, Node.PROPAGATE)状态要在一瞬间执行完shouldParkAfterFailedAcquire把waitStatus修改为SIGNAL,这算是对一种短暂情况下的优化。

AQS双向队列其实里面不仅包含了独占锁,也包含了共享锁,这个队列中2种模式是可以同时存在的。

共享模式的调用其实和独占模式相当类似,唯一不同的是共享模式的锁可以被多个线程共享,而独占模式下锁同一时刻只能被一个线程拥有。

共享模式下,头节点获取共享锁后可以立即唤醒后继节点,而不用等待获取共享锁后释放再唤醒,唤醒后继线程有2处,一处是获取到共享锁后可以立即唤醒后续的线程,但是后续线程必须是共享模式的线程;第二处是在释放锁后唤醒后继线程,这边我认为释放锁后唤醒的后继线程可以包含独占模式,但是前提是所有的独占模式前面所有的共享模式锁都已经释放。

以上是关于深入理解AQS(二)- 共享模式的主要内容,如果未能解决你的问题,请参考以下文章

深入理解AQS

进阶笔录-深入理解Java线程之-AQS

深入理解设计模式-享元模式

深入理解设计模式-享元模式

Java并发:深入浅出AQS之共享锁模式源码分析

深入理解AbstractQueuedSynchronizer