一、共享式锁实现及应用
上一篇文章分享了独占式获或释放同步状态的过程。本文分享AQS共享式获取或释放同步状态,共享式获取或释放过程与独占式获取或释放过程大体相同,按照相同流程进行分析。首先,创建一个读锁类,用于对资源的共享式读访问,其核心是创建继承自AQS的内部类,并重写tryAcquireShared和tryReleaseShared方法。读锁类实现锁接口,并创建同步器成员变量,为简便起见主要实现了Lock接口中的lock和unlock方法。
读锁实现:
public class TestSharedReadLock implements Lock {
private MySync sync;
private static Integer permits;
public TestSharedReadLock(Integer permits) {
this.permits = permits;
sync = new MySync(permits);
}
public static class MySync extends AbstractQueuedSynchronizer {
public MySync(int permits) {
setState(permits);
}
@Override
protected int tryAcquireShared(int arg) {
for(;;) {
int curPermits = getState();
int postPermits = curPermits - arg;
if(postPermits < 0 || compareAndSetState(curPermits, postPermits)) {
return postPermits;
}
}
}
@Override
protected boolean tryReleaseShared(int arg) {
for(;;) {
int curPermits = getState();
if(curPermits == permits) {
throw new IllegalMonitorStateException();
}
int postPermits = curPermits + arg;
if(compareAndSetState(curPermits, postPermits)) {
return true;
}
}
}
}
@Override
public void lock() {
sync.acquireShared(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
sync.releaseShared(1);
}
@Override
public Condition newCondition() {
return null;
}
}
对该读锁做简单测试,将其应用到如下场景中:
public class TestReadLockCase {
private static TestSharedReadLock readLock = new TestSharedReadLock(3);
public static class Task implements Runnable {
@Override
public void run() {
readLock.lock();
System.out.println(Thread.currentThread().getName() + ": 进入lock");
try {
Thread.sleep(2000);
} catch (Exception e) {
} finally {
}
readLock.unlock();
System.out.println(Thread.currentThread().getName() + ": 释放lock");
}
}
public static void main(String ... args) {
Instant startInstant = Instant.now();
List<Thread> threads = new ArrayList<>();
for(int i=0; i< 10; i++) {
threads.add(new Thread(new Task(), "Thread-" + i));
}
threads.stream().forEach(t -> {
t.start();
});
threads.stream().forEach(t -> {
try {
t.join();
} catch (Exception e) {
}
});
System.out.println("cost = " + Duration.between(startInstant, Instant.now()).toMillis() + " ms");
}
}
输出结果:
Thread-0: 进入lock
Thread-2: 进入lock
Thread-1: 进入lock
Thread-2: 释放lock
Thread-3: 进入lock
Thread-0: 释放lock
Thread-1: 释放lock
Thread-5: 进入lock
Thread-4: 进入lock
Thread-3: 释放lock
Thread-4: 释放lock
Thread-7: 进入lock
Thread-6: 进入lock
Thread-5: 释放lock
Thread-8: 进入lock
Thread-7: 释放lock
Thread-6: 释放lock
Thread-8: 释放lock
Thread-9: 进入lock
Thread-9: 释放lock
cost = 8094 ms
以上场景模拟了共享式获取读锁的场景,同一时间最多允许3个线程获取锁作业,其它线程阻塞等待,模拟场景中开启了10个线程获取锁,模拟任务处理耗时2000ms,每批3个执行线程,分4批完成,耗时8094ms。下文借这个应用示例对共享式获取锁和释放锁的逻辑进行分析。
二、共享式获取
在示例中,读锁的lock逻辑调用了AQS中共享式获取的模板方法是acquireShared,方法源码如下:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
主要逻辑:
- a. tryAcquireShared,调用用户实现的tryAcquireShared方法,返回同步状态>=0表示当前线程获取同步状态成功;返回<0表示获取同步状态失败
- b. doAcquireShared,(addWaiter)如果当前线程获取同步状态失败,则将当前线程封装成节点添加到双向链表尾部;如果当前节点的前驱节点是头节点,则再次尝试共享式获取同步状态;如果再次获取失败,则判断是否将当前节点挂起;将节点挂起并检查中断状态;
acquireShared方法首先调用我们自定义的tryAcquireShared方法:
protected int tryAcquireShared(int arg) {
for(;;) {
int curPermits = getState();
int postPermits = curPermits - arg;
if(postPermits < 0 || compareAndSetState(curPermits, postPermits)) {
return postPermits;
}
}
}
改方法返回当前同步状态的值,通过无限循和CAS保证在并发情况下修改同步状态值成功,如果返回同步状态>=0则表示获取同步状态成功,如果返回同步状态<0则表示获取同步状态失败。
获取同步状态失败后,调用doAcquireShared方法,该方法与独占是获取逻辑中acquireQueued的处理过程类似,只是将addWaiter方法也封装在了方法体中。doAcquireShared方法描述如下:
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
其中addWaiter方法逻辑与独占式获取相同,不再赘述。doAcquireShared方法中剩余逻辑也基本和acquireQueued方法一致,只是调用获取同步状态方法及后续处理逻辑稍有不同,doAcquireShared方法中调用共享式获取tryAcquireShared方法,acquireQueued中调用独占式获取tryAcquire方法,另外,acquireQueued方法中如果获取同步状态成功则将当前节点设置为头节点,doAcquireShared方法中如果获取同步状态成功调用setHeadAndPropagate方法,其逻辑如下。shouldParkAfterFailedAcquire、parkAndCheckInterrupt等方法分析见上一篇文章。
setHeadAndPropagate方法描述如下:
Node h = head; // Record old head for check below
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
主要逻辑:
- a. setHead,将节点设置为头节点
- b. 如果剩余同步状态(资源)>0,或头结点的状态<0(SIGNAL、PROPAGATE),则判断其后继节点,如果后继节点为空或SHARED(SHARED状态通过共享类的静态变量实现),则继续通过doReleaseShared唤醒头节点的后继节点
doReleaseShared方法描述如下:
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
如果头节点的状态为SIGNAL,则将头结点状态设置为0并唤醒后继节点,然后再将节点状态设置为PROPAGATE,便于其它竞争线程通过setHeadAndPropagate方法和doReleaseShared将PROPAGATE状态向后传播。注意,当有多个节点释放状态或被唤醒的后继节点竞争成功时,该方法存在并发情况,如果在修改头结点状态的过程中头结点被其它节点重新设置,则当前线程继续循环设置新头结点的状态。
三、共享式释放
在示例中,读锁的unlock逻辑调用了AQS中独占式获取的模板方法是releaseShared,方法源码如下:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
主要逻辑:
- a. tryReleaseShared,调用用户实现的tryReleaseShared方法释放同步状态,返回成功表示释放同步状态成功
- b. doReleaseShared,唤醒后继节点并将状态设置为PROPAGATE
releaseShared方法首先调用用户实现的tryReleaseShared方法,如下:
protected boolean tryReleaseShared(int arg) {
for(;;) {
int curPermits = getState();
if(curPermits == permits) {
throw new IllegalMonitorStateException();
}
int postPermits = curPermits + arg;
if(compareAndSetState(curPermits, postPermits)) {
return true;
}
}
}
并发情况下,同步状态可能已经被修改,因此tryReleaseShared方法中如果当前资源等于准入资源,则抛出异常,否则通过无限循和CAS保证在并发情况下修改同步状态值成功。
tryReleaseShared方法前面已经分析过,不在赘述。