AbstractQueuedSynchronizer简介
AbstractQueuedSynchronizer提供了一个FIFO队列,可以看做是一个可以用来实现锁以及其他需要同步功能的框架。这里简称该类为AQS。AQS的使用依靠继承来完成,子类通过继承自AQS并实现所需的方法来管理同步状态。例如ReentrantLock,CountDownLatch等。其中,我们最常用的大概就是ReentrantLock和CountDownLatch了。ReentrantLock提供了对代码块的并发访问控制,也就是锁,说是锁,但其实并没有用到关键字synchronized,这么神奇?其实其内部就是基于同步器来实现的,本文结合ReentrantLock的使用来分析同步器独占锁的原理。
AQS的两种功能
从使用上来说,AQS的功能可以分为两种:独占和共享。对于这两种功能,有一个很常用的类:ReentrantReadWriteLock,其就是通过两个内部类来分别实现了这两种功能,提供了读锁和写锁的功能。但子类实现时,只能实现其中的一种功能,即要么是独占功能,要么是共享功能。
对于独占功能,例如如下代码:
ReentrantLock lock = new ReentrantLock();
...
public void function(){
lock.lock();
try {
// do something...
} finally {
lock.unlock();
}
}
这个很好理解,通过ReentrantLock来保证在lock.lock()之后的代码在同一时刻只能有一个线程来执行,其余的线程将会被阻塞,直到该线程执行了lock.unlock()。这就是一个独占锁的功能。
对于共享功能,例如如下代码:
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
...
public void function(){
lock.readLock().lock();
try {
// do something...
} finally {
lock.readLock().unlock();
}
}
代码中的lock是ReentrantReadWriteLock类的实例,而lock.readLock()为获取其中的读锁,即共享锁,使用方式并无差别,但和独占锁是有区别的:
- 读锁与读锁可以共享
- 读锁与写锁不可以共享(排他)
- 写锁与写锁不可以共享(排他)
AQS独占锁的内部实现
AQS的主要数据结构
由于使用AQS可以实现锁的功能,那么下面就要分析一下究竟是如何实现的。
AQS内部维护着一个FIFO的队列,该队列就是用来实现线程的并发访问控制。队列中的元素是一个Node类型的节点,Node的主要属性如下:
static final class Node {
int waitStatus;
Node prev;
Node next;
Node nextWaiter;
Thread thread;
}
AQS中有一个state变量,该变量对不同的子类实现具有不同的意义,对ReentrantLock来说,它表示加锁的状态:
- 无锁时state=0,有锁时state>0;
- 第一次加锁时,将state设置为1;
- 由于ReentrantLock是可重入锁,所以持有锁的线程可以多次加锁,经过判断加锁线程就是当前持有锁的线程时(即exclusiveOwnerThread==Thread.currentThread()),即可加锁,每次加锁都会将state的值+1,state等于几,就代表当前持有锁的线程加了几次锁;
解锁时每解一次锁就会将state减1,state减到0后,锁就被释放掉,这时其它线程可以加锁;当持有锁的线程释放锁以后,如果是等待队列获取到了加锁权限,则会在等待队列头部取出第一个线程去获取锁,获取锁的线程会被移出队列;
state变量定义如下:
/**
* The synchronization state.
*/
private volatile int state;