背景知识: 理解LimitLatch背后的AQS
回顾AQS
一般我看源码惯性都会先看源码上面的注释, 如果没有在大致看结构,LimitLatch上面有注释:
Shared latch that allows the latch to be acquired a limited number of times after which all subsequent requests to acquire the latch will be placed in a FIFO queue until one of the shares is returned
共享栅栏允许栅栏被获取有限的次数,在此之后(即达到次数限制后),所有后续尝试获取该锁存器的请求都将被放入一个先进先出(FIFO)队列中,直到其中一个“共享份额”(share)被归还(释放)。我在看LimitLatch时候看到了一个熟悉的面孔,也就是AbstractQueuedSynchronizer,在LimitLatch中定义了一个内部类:
private class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 1L;
public Sync() {
}
@Override
protected int tryAcquireShared(int ignored) {
long newCount = count.incrementAndGet();
if (!released && newCount > limit) {
// Limit exceeded
count.decrementAndGet();
return -1;
} else {
return 1;
}
}
@Override
protected boolean tryReleaseShared(int arg) {
count.decrementAndGet();
return true;
}
}
调用countUpOrAwait的源码如下所示:
public void countUpOrAwait() throws InterruptedException {
if (log.isDebugEnabled()) {
log.debug("Counting up["+Thread.currentThread().getName()+"] latch="+getCount());
}
sync.acquireSharedInterruptibly(1);
}
回忆一下AQS
acquireSharedInterruptibly来自AbstractQueuedSynchronizer, 我们回忆一下AQS相关的知识,首先AQS提供了什么?提到AQS的时候,我想到的是这提供了一个线程协调框架,也就是说线程是交错执行的,我们希望线程之间协作,在到达某些条件之后,一些线程才能接着执行。这样说可能有点抽象,日常生活的例子就是饭店坐满之后,客户等待。这说的也是上面的LimitLatch,在到达最大连接数之后进行等待,满足条件之后才接着执行。于是我们回头接着看AbstractQueuedSynchronizer的注释看能不能给出一个更好的定义, 在AbstractQueuedSynchronizer中我们可以看到下面的注释:
Provides a framework for implementing blocking locks and related synchronizers (semaphores, events, etc) that rely on first-in-first-out (FIFO) wait queues.
为实现依赖于先进先出(FIFO)等待队列的阻塞锁和相关同步器(如semaphores、事件等)提供了一个基础框架。
我们将上面的话可以理解为了实现锁和相关同步器,实现一个锁需要什么呢? 我们观察一下synchronized提供的机制,同时只有一个线程可以进入同步方法或同步块,这个线程可以被认为成功的获取了锁,其他的线程获取这个锁会进入到一个队列里面,获取锁的线程从同步块出来之后,会尝试再获取锁。直到到达某些条件。
那么AQS里面首先内置了一个队列,然后拿怎么判断获取锁成功了没有呢? 或者满足某些条件没有呢?让我们接着看AbstractQueuedSynchronizer的注释:
This class is designed to be a useful basis for most kinds of synchronizers that rely on a single atomic int value to represent state.
该类旨在为大部分基于单一原子整型值表示状态的同步器提供实用的基础框架
从这句话可以提取出来AQS选择用一个原子整型变量来表示状态,那怎么判断是否满足某些条件呢?
通过实例来学习
让我们看一个代码示例:
public class Mutex implements Lock, java.io.Serializable {
private static class Sync extends AbstractQueuedSynchronizer {
protected boolean isHeldExclusively() {
return getState() == 1;
}
public boolean tryAcquire(int acquires) {
assert acquires == 1;
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
protected boolean tryRelease(int releases) {
assert releases == 1;
if (getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
Condition newCondition() {
return new ConditionObject();
}
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}
private final Sync sync = new Sync();
public void lock() {
sync.acquire(1);
}
public boolean tryLock() {
return sync.tryAcquire(1);
}
public void unlock() {
sync.release(1);
}
public Condition newCondition() {
return sync.newCondition();
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
public boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
}
我们可以清楚的看到Sync继承了AbstractQueuedSynchronizer,然后判断当前锁是否可以获取是通过Sync的isHeldExclusively方法,而isHeldExclusively的逻辑很简单就是判断getState的返回值是否等于1。那与之相反0就代表当前锁没有被其他线程获取。lock获取锁的时候调用的是acquire方法, 这就调用到父类上面了:
所以在获取锁的时候是调用子类重写的tryAcquire方法,通过CAS将当前变量写为1,同时独占线程的引用。释放锁的时候的调用链路如下所示:
里面的逻辑也就是将AQS父类对独占线程的引用置空,然后将状态变量设置为0。这也就是一个解锁过程。通过这个例子我们可以知道AQS可以帮我们做什么,她抽象了一套同步框架,通过tryAcquire判断当前是否满足条件,如果满足条件就往下执行,执行完之后通过tryRelease释放许可,让其他线程可以获取这个许可。
共享模式
这其实是AQS的独占模式,与独占相对的是共享模式,所谓独占意思指的是同时只有一个线程获取许可,或者说只有一个许可证。而共享模式意味着许可证有多个,那怎么发放多个许可证呢?答案还在这个整型原子变量上,int的数值就代表了许可证或者任务数,只不过共享模式下面,子类应当实现tryAcquireShared、tryReleaseShared这两个方法。让我们来举一个例子来说明共享模式是如何运行的,以CountDownLatch为例, 我们初始化的时候就填入了任务的数量:
CountDownLatch countDownLatch = new CountDownLatch(50);
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
Sync(int count) {
setState(count);
}
每次完成一个任务,我们通过CountDownLatch的countDown方法来将任务数减一:
public void countDown() {
sync.releaseShared(1);
}
这个sync是CountDownLatch中内部类Sync的实例:
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
完成一个任务,调用countDown将state减一,判断任务有没有完成通过CountDownLatch的await方法,如果没完成当前线程会进入 java.lang.Thread.State: WAITING (parking)状态。
总结一下AQS为我们提供了什么
AQS抽象了同步的场景,同步的语义就是等待,不满足某些条件时,一些线程在创造条件,一些线程在等待条件的完成,等待条件完成的线程进入等待状态加入到队列当中。JDK里面的ReentrantLock、Semaphore、CyclicBarrier 都是这样的场景,我们使用ReentrantLock来保护资源,这里的条件可以引申为锁的释放。那Semaphore呢? Semaphore对应的场景就是景区的门票不能无限制的兜售,景区会对游客的数量进行限制,出来一个才能进去一个。示例如下:
System.out.println("当前只能兜售三张门票");
Semaphore semaphore = new Semaphore(3);
Thread t1 = new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "成功抢到");
semaphore.release();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "thread-1");
Thread t2 = new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "成功抢到");
semaphore.release();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "thread-2");
Thread t3 = new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "成功抢到");
semaphore.release();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "thread-3");
Thread t4 = new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "成功抢到");
semaphore.release();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "thread-4");
t1.start();
t2.start();
t3.start();
t4.start();
try {
t1.join();
t2.join();
t3.join();
t4.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
如果想对接口进行限流,简单一点可以用Semaphore进行简单的限制。Semaphore、CountCownLatch、ReentrantLock是一组线程里面的一方在等待另一方,而CyclicBarrier 描述的场景则是这一组线程都在互相等待,模拟现实的场景就是开会:
CyclicBarrier cyclicBarrier = new CyclicBarrier(3,()->{
System.out.println("所有人到齐开始开会");
});
Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
System.out.println("第一个参会者到达");
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
throw new RuntimeException(e);
}
},"thread-a");
Thread t2 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(4);
System.out.println("第二个参会者到达");
cyclicBarrier.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
},"thread-b");
t1.start();
t2.start();
cyclicBarrier.await();
我总是喜欢不断的回顾,不断的审视前面学过的概念,因为我觉得知识在不断的凝练中变得清晰。我的感觉是抽象的概念需要附着在具体的实例上面,这会让概念好理解一些。 毕竟概念是具体的抽象,我想起《高等代数简明教程》的一句话,在解决实际问题的时候,我们需要将具体转换为抽象,也需要将抽象转换为具体。
接着来看LimitLatch
上面我们复习了一下AQS,我们借着来看LimitLatch就显得简单一些了:
// 来自LimitLatch
public void countUpOrAwait() throws InterruptedException {
if (log.isDebugEnabled()) {
log.debug("Counting up["+Thread.currentThread().getName()+"] latch="+getCount());
}
sync.acquireSharedInterruptibly(1);
}
// 来自AQS
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
// 来自LimitLatch
protected int tryAcquireShared(int ignored) {
long newCount = count.incrementAndGet();
if (!released && newCount > limit) {
// Limit exceeded
count.decrementAndGet();
return -1;
} else {
return 1;
}
}
其实也就是达到最大连接数之后进入等待。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。