ReentrantLock和Semaphore这两个接口之间存在许多共同点,这两个类都已用做一个“阀门”,即每次只允许一定数量的线程通过,并当线程到达阀门时,可以通过(在调用lock或acquire时成功返回),也可以等待(在调用lock或acquire时阻塞),还可以取消(在调用tryLock或tryAuquire时返回”假”,表示在指定的时间内锁时不可用的或无法获得许可)。
而且,这两个接口都支持可中断的,不可中断的以及限时的获取操作,也支持等待线程执行公平或非公平的队列操作。
我们可以用锁来实现计数信号量,以及可以通过计数信号量来实现锁
这并非java.util.concurrent.Semaphore的真实现方式
// 14-12 使用Lock来实现信号量
@ThreadSafe
public class SemaphoreOnLock {
private final Lock lock=new ReentrantLock();
//条件谓词:permitsAvailable(permits>0)
private final Condition permitsAvailable=lock.newCondition();
private int permits;
SemaphoreOnLock(int initialPermits) {
lock.lock();
try{
permits=initialPermits;
}finally {
lock.unlock();
}
}
//阻塞直到:permitsAvailable
public void acquire() throws InterruptedException{
lock.lock();
try{
while(permits<=0)
permitsAvailable.await();
--permits;
}finally{
lock.unlock();
}
}
public void release(){
lock.lock();
try{
++permits;
permitsAvailable.signal();
}finally {
lock.unlock();
}
}
}
事实上,ReentrantLock和Semaphore都使用了一个共同的基类,即AbstractQueueSynchronizer(AQS),这个类也是其他许多同步类的基类。
AQS是一个用于构建锁和同步器的框架,许多同步器都可以通过AQS很容易并且高效地构造出来。不仅ReentrantLock和Semaphore是基于AQS构建的,
还包括CountDownLatch、ReentrantReadWriteLock、SynchronousQueue和FutureTask。
AQS解决了在实现同步器时设计的大量细节问题,例如等待线程采用FIFO队列操作顺序。在不同的同步器还可以定义一些灵活的标准来判断某个线程是应该通过还是等待。
基于AQS来构建同步器能带来许多好处。它不仅能极大地减少实现工作,而且也不必处理在多个位置上的竞争问题。
在基于AQS构建的同步器中,只可能在一个时刻发生阻塞,从而降低上下文切换的开销,并提高吞吐量、在设计AQS是充分考虑了可伸缩性。因此java.utilconcurrent中所有基于AQS构建的同步容器都能获得这个优势。
多数情况下不会直接使用AQS,标准同步器类集合能满足绝大多数情况的需求。
在基于AQS构建的同步器类中,最基本的操作包括各种形式的获取操作和释放操作。
获取操作是一种依赖状态的操作,并且通常会阻塞。
在使用锁或信号量时,获取(acquire)操作获得的是锁或许可,并且调用者可能会一直等待直到同步器类处于可被获取的状态。
在使用CountDownLatch时,获取(countDown)操作意味这“等待并直到闭锁到达结束状态”
使用FutureTask时,获取(get)操作意味着“等待并直到任务已经完成”。
释放并不是一个可阻塞的操作,当执行释放操作时,所有在请求时被阻塞的线程都会开始执行。
如果一个类想成为状态依赖的类,必须拥有一些状态。
AQS负责管理同步器类中的状态,它管理了一个整数状态信息,可以通过getState,setState以及compareAndSetState等protected类型方法来进行操作。
ReentrantLock用它来表示所有者线程已经重复获取该锁的次数。
Semaphore用它表示剩余的许可数量(acquire和release操作)。
FutureTask用它来表示任务的状态(尚未开始,正在运行,以完成以及已取消)。
还可以自行管理一些额外的状态变量,例如,ReentrantLock保存了锁的当前所有者的信息,这样就能区分某个操作是重入还是竞争的。
下面给出了AQS中的获取操作与释放操作的形式。
根据同步器的不同,获取操作可以是一种独占操作(例如ReentrantLock)也可以是一个非独占操作(例如Semaphore和CountDownLatch)。
// 14-13 AQS中获取操作与释放操作的标准形式
boolean acquire() throws InterruptedException {
while (当前状态不允许获取操作) {
if (需要阻塞获取请求) {
如果当前线程不在队列中,则将其插入队列
阻塞当前线程
}
else
返回失败
}
可能更新同步器的状态
如果线程处于队列中,则将其移出队列
返回成功
}
void release() {
更新同步器的状态
if (新的状态允许某个被阻塞的线程获取成功)
解除队列中一个或多个线程阻塞状态
}
如果某个同步器支持独占的获取操作,那么需要实现一些保护方法,包括tryAcquire,tryRelease和isHeldExclusively等
对于支持共享获取的同步器,则应该实现tryAuquireShared和tryReleaseShared。
AQS中的acquire, acquireShared, release和releaseShared等方法都将调用这些方法在子类中带有前缀try的版本来判断某个操作是否能执行。
在同步器的子类中,可以根据其获取操作和释放操作的语义,使用getState,setState以及compareAndSetState来检查和更新状态,并通过返回的状态值来告知基类“获取”或“释放”同步器的操作是否成功。
14-14是一个使用AQS实现的二元闭锁。它包含两个公有方法:await和signal,分别对应获取操作和释放操作。
起初,闭锁是关闭的,任何调用await的线程都将阻塞并直到闭锁被打开。当通过调用signal打开闭锁时,所有等待中的线程都将被释放,并且随后到达闭锁的线程也被允许执行。
// 14-14 使用AbstractQueueSynchronizer实现的二元锁
@ThreadSafe
public class OneShotLatch {
private final Sync sync=new Sync();
//AQS中的acquire, acquireShared, release和releaseShared等方法都将调用这些方法在子类中带有前缀try的版本来判断某个操作是否能执行。
public void signal(){
sync.releaseShared(0);
}
//起初,闭锁是关闭的(0),任何调用await的线程都将阻塞并直到闭锁被打开
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(0);
}
private class Sync extends AbstractQueuedSynchronizer{
protected int tryAcquireShared(int ignored){
//如果闭锁时打开的(state==1),那么这个操作将成功,否则将失败
return (getState()==1)?1:-1;
}
//在tryReleaseShared中将闭锁状态设置为打开,(通过返回值)表示该同步器处于完全被释放的状态,因而AQS让所有等待中的线程都重新尝试请求该同步器,并且由于tryReleaseShared将返回成功,因此现在的请求操作将成功
protected boolean tryReleaseShared(int ignored){
setState(1); //现在打开闭锁
return true; //现在其他的线程可以获取该闭锁
}
}
}
在OneShotLatch中,AQS状态用来表示闭锁状态——关闭(0)或打开(1)。await方法调用AQS的acquireSharedInterruptibly,然后接着调用OneShotLatch中的tryAcquireShared方法。
在tryAcquireShared的实现中必须返回一个值来表示这个该获取操作能否执行。
如果之前已经打开了闭锁,那么tryAcquireShared将返回成功并允许线程通过,否则就会返回一个表示获取操作失败的值。
acquireSharedInterruptibly处理失败的方式,是把这个线程放入等待队列中。
类似地,signal将调用releaseShared,接下来又会调用tryReleaseShared。
在tryReleaseShared中将闭锁状态设置为打开,(通过返回值)表示该同步器处于完全被释放的状态,因而AQS让所有等待中的线程都重新尝试请求该同步器,并且由于tryReleaseShared将返回成功,因此现在的请求操作将成功。
java.util.concurrent中的许多阻塞类,例如ReentrantLock,Semaphore,ReentrantReadWriteLock,CountDownLatch,SynchronousQueue和FutureTask等,都是基于AQS构建的。
ReentrantLock只支持独占方式的获取操作,因此它实现了tryAcquire,tryRelease和isHeldExclusively,14-15给出了非公平版本的tryAcquire。ReentrantLock将同步状态用于保存锁获取操作的次数,并且还维护一个owner变量来保存当前所有者线程的标识符,只有在当前线程刚刚获取到锁,或者正要释放锁额时候,才会修改这个变量。
在tryRelease中检查owner域,从而确保当前线程正在执行unlock操作之前已经获取了锁:在tryAcquire中将使用这个域来区分获取操作时重入的还是竞争的。
// 14-15 基于非公平的ReentrantLock实现tryAcquire
protected boolean tryAcquire(int ignored) {
final Thread current = Thread.currentThread();
int c = getState();
//如果锁未被持有,那么它将尝试更新锁的状态以表示锁已经被持有。
if (c == 0) {
//使用compareAndSetState来原子地更新状态,表示这个锁已经被占有,并确保状态在最后一次检查以后就没有被修改过
if (compareAndSetState(0, 1)) {
owner = current;
return true; //(通过返回值)表示该同步器处于完全被释放的状态
}
} else if (current == owner) {//如果锁状态表明它已经被持有,并且如果当前线程是锁的拥有者,那么获取计数会递增,如果当前线程不是锁的拥有者,那么获取操作将失败
setState(c+1);
return true;
}
return false;
}
在一个线程尝试获取锁时,tryAcquire将首先检查锁的状态。
如果锁未被持有,那么它将尝试更新锁的状态以表示锁已经被持有。
由于状态可能在检查后被立即修改,因此tryAcquire使用compareAndSetState来原子地更新状态,表示这个锁已经被占有,并确保状态在最后一次检查以后就没有被修改过。
根据15.2可以知道,如果c的值在这个过程中没有被修改,仍为0,则变为1,表示这个锁已经被占有,并返回true,否则返回false。这是为了避免状态在检查后立即被修改。
如果锁状态表明它已经被持有,并且如果当前线程是锁的拥有者,那么获取计数会递增,如果当前线程不是锁的拥有者,那么获取操作将失败。
ReentrantLock还利用了AQS对多个条件变量和多个等待线程集的内置支持。Lock.newCondition将返回一个新的ConditionObject实例,这是AQS的一个内部类。
Semaphore将AQS的同步状态用于保存当前可用许可的数量。
14-16中的tryAcquireShared方法首先计算剩余许可的数量,如果没有足够的许可,那么会返回一个值表示获取操作的失败。
如果还有剩余的许可,那么tryAcquireShared会通过compareAndSetState以原子方式来降低许可的计数。
如果这个操作成功(意味着许可的计数自从上一次读取后就没有被修改过)那么将返回一个值表示获取操作成功。在返回值中还包含了表示其他共享获取操作能否成功的信息,如果成功,那么其他等待的线程同样会解除阻塞。
CAS(compareAndSet)算法的过程是这样:它包含3个参数CAS(V,E,N)。V表示要更新的变量(内存值),E表示预期值(旧的),N表示新值。当且仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。
// 14-16 Semaphore中的tryAuquireShared与tryReleaseShared
protected int tryAcquireShared(int acquires) {
while (true) {
int available = getState();
int remaining = available - acquires; //首先计算剩余许可的数量
//如果还有剩余的许可,那么tryAcquireShared会通过compareAndSetState以原子方式来降低许可的计数。
if (remaining < 0
|| compareAndSetState(available, remaining))
//当没有足够的许可,或者当tryAuquireShared可以通过原子方式来更新许可的计数以响应获取操作时,while循环将终止。
return remaining;
}
}
protected boolean tryReleaseShared(int releases) {
while (true) {
int p = getState();
if (compareAndSetState(p, p + releases))
return true;
}
}
当没有足够的许可,或者当tryAuquireShared可以通过原子方式来更新许可的计数以响应获取操作时,while循环将终止。
tryReleaseShared将增加许可计数,这可能会解除等待中线程阻塞状态,并且不断尝试直到更新操作成功。
tryReleaseShared的返回值表示在这次释放操作中解除了其他线程的阻塞。
CountDownLatch使用AQS的方式与Semaphore很相似:在同步状态中保存的是当前的计数值。countDown方法调用release,从而导致计数值递减,并且当计数值为0时,解除所有等待线程的阻塞,await调用acquire方法,当计数器为0时,acquire立即返回,否则将阻塞。
Future.get的语义非常类似与闭锁的语义——如果发生了某个事件(由于FutureTask表示的任务执行完成或被取消),那么线程就可以恢复执行,否则这些线程将停留在队列中直到事件发生。
FutureTask中AQS的同步状态被用来保存任务的状态。例如,正在运行,已完成或已取消。FutureTask还维护一些额外的状态变量,用来保存计算或抛出的异常。
此外,它还维护了一个引用,指向正在执行任务的线程(如果它当前处于运行状态),因而如果任务取消,线程就会中断
ReadWriteLock接口表示存在两个锁:一个读取锁和一个写入锁,但在基于AQS实现的ReentrantReadWriteLock中,单个AQS子类将同时管理读取加锁和写入加锁。
ReentrantReadWriteLock分别使用了两个16位的状态来表示写入锁的计数和读取锁的计数。
在读取锁啥好过你的操作将使用共享的获取方法和释放方法,在写入锁上的操作将使用独占的获取方法与释放方法。
AQS在内部维护一个等待线程队列,其中记录了某个线程请求的是独占访问还是共享访问。
在ReentrantReadWriteLock中,当锁可用时,如果位于队列头部的线程执行写入操作,那么线程会获得这个锁,
如果位于队列头部的线程执行读取访问,那么队列中在第一个写入线程之前的所有线程都将获得这个锁。