AbstractQueuedSynchronizer知识梳理
1.AbstractQueuedSynchronizer
在基于AQS构建的同步器类中,最基本的操作包括各种形式的获取操作和释放操作。获取操作是一种依赖状态的操作,并且通常会阻塞。释放操作并不是一个可阻塞的操作,当执行释放操作时,所有在请求时被阻塞的线程都会开始执行。
AQS负责管理同步器类中的状态,它管理了一个整数状态信息,可以通过getState、setState以及compareAndSetState等protected类型方法来进行操作。这个整数可以用于表示任意状态。例如,ReentrantLock用它来表示所有者线程已经重复获取该锁的次数,Semaphore用它来表示剩余的许可数量,FutureTask用它来表示任务的状态(尚未开始、正在运行、已完成以及已取消)。
下面给出AQS的获取操作与释放操作的形式:
//AQS的获取操作与释放操作的标准形式
boolean acquire() throws InterruptedException {
while (当前状态不允许获取操作) {
if (需要阻塞获取请求) {
如果当前线程不再队列中,则将其插入队列
阻塞当前线程
}
else
返回失败
}
可能更新同步器的状态
如果线程位于队列中,则将其移出队列
返回成功
}
void release() {
更新同步器的状态
if (新的状态允许某个被阻塞的线程获取成功)
接触队列中一个或多个线程的阻塞状态
}
上面示例中,首先,同步器判断当前状态是否允许获得操作,如果是,则允许线程执行,否则获取操作将阻塞或失败。其次,就是更新同步器的状态,获取同步器的某个线程可能会对其他线程能否也获取该同步器造成影响。
如果某个同步器支持独占的获取操作,那么需要实现一些protected方法,包括tryAcquire、tryRelease和isHeldExclusively等,而对于支持共享获取的同步器,则应该实现tryAcquireShared和tryReleaseShared等方法。
下面示例不通过已有闭锁的类库来实现一个简单的闭锁:
//使用AbstractQueuedSynchronizer实现的二元闭锁
public class OneShotLatch {
private final Sync sync = new Sync();
public void signal() {
sync.releaseShared(0);
}
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;
}
protected boolean tryReleaseShared(int ignored) {
setState(1); //现在打开闭锁
return true; //现在其他的线程可以获取该闭锁
}
}
}
2.java.util.concurrent同步器类中的AQS
2.1ReentrantLock
ReentrantLock只支持独占方式的获取操作,因此它实现了tryAcquire、tryRelease和isHeldExclusively。ReenrantLock将state变量用于保存锁获取操作的次数,并且还维护一个owner变量来保存当前所有者线程的标识符,只有在当前线程刚刚获取到锁,或者正要释放锁的时候,才会修改这个变量。
如下代码为非公平版本的tryAcquire:
//基于非公平的ReentrantLock实现tryAcquire
protected boolean tryAcquire(int ignored) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, 1)) {
owner = current;
return true;
}
} else if (current == owner) {
setState(c + 1);
return true;
}
return false;
}
2.2Semaphore
Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。Semaphore将AQS的同步状态用于保存当前可用许可的数量。
Semaphore可以用于流量控制,特别是公用资源有限的应用场景。
假如有一个需求,要读取几万个文件的数据,因为都是I/O密集型任务,我们可以启动几十个线程并发地读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这时我们必须控制只有10个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。这个时候可以使用Semaphore做流量控制,如下代码:
public class SemaphoreTest {
private static final int THREAD_COUNT = 30;
private static ExecutorServicethreadPool = Executors.newFixedThreadPool(THREAD_COUNT);
private static Semaphore s = new Semaphore(10);
public static void main(String []args) {
for (int i = 0 ; i < THREAD_COUNT ; ++i) {
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
s.acquire();
System.out.println("save data");
s.release();
}
catch (InterruptedException e) {}
}
});
}
threadPool.shutdown();
}
}
Semaphore的用法很简单,首先线程使用Semaphore的acquire()方法获取一个许可证,使用完之后调用release()方法归还许可证。还可以用tryAcquire()方法尝试获取许可证。
2.3CountDownLatch
CountDownLatch允许一个或多个线程等待其他线程完成操作。
public class CountDownLatchTest {
static CountDownLatch c = new CountDownLatch(2);
public static void main(String []args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("1");
c.countDown();
System.out.println("2");
c.countDown();
}
}).start();
c.await();
System.out.println("3");
}
}
CountDownLatch的构造函数接收一个int类型的参数作为计数器,如果想等待N个点完成,这里就传入N。
当调用CountDownLatch的countDown
方法时,N就会减1。调用CountDownLatch的await
方法会阻塞当前线程,直到N变成零。
计数器必须大于等于0,等于0的时候,调用await方法时不会阻塞当前线程。
注意:CountDownLatch不可能重新初始化或者修改CountDownLatch对象的内部计数器的值。
2.4FutureTask
Future.get的语义非常类似于闭锁的语义——如果发生了某件事件(由FutureTask表示的任务执行完成或被取消),那么线程就可以恢复执行,否则这些线程将停留在队列中并直到该事件发生。
在FutureTask中,AQS同步状态被用来保存任务的状态,例如,正在运行、已完成或已取消。
2.5ReentrantReadWriteLock
ReadWriteLock接口表示存在两个锁:一个读取锁和一个写入锁,但在基于AQS实现的ReentrantReadWriteLock中,单个AQS子类将同时管理读取加锁和写入加锁。ReentrantReadWriteLock使用了一个16位的状态来表示写入锁的计数,并且使用了另一个16位的状态来表示读取锁的计数。在读取锁上的操作将使用共享的获取方法与释放方法,在写入锁上的操作将使用独占的获取方法与释放方法。
下图为读写锁状态的划分方式:
当前同步状态表示一个线程已经获取了写锁,且重进入了两次,同时也连续获取了两次读锁,
AQS在内部维护一个等待线程队列,其中记录了某个线程请求的是独占访问还是共享访问。在ReentrantReadWriteLock中,当锁可用时,如果位于队列头部的线程执行写入操作,那么线程会得到这个锁,如果位于队列头部的线程执行读取操作,那么队列中第一个写入线程之前的所有线程都将获得这个锁。