7.手把手教你用AQS来实现锁
在使用AQS之前,我们首先需要了解它。
AQS是一个抽象类,不可以被实例化,它的设计之初就是为了让子类通过继承来实现多样的功能的。它内部提供了一个FIFO的等待队列,用于多个线程等待一个事件(锁)。它有一个重要的状态标志——state,该属性是一个int值,表示对象的当前状态(如0表示lock,1表示unlock)。AQS提供了三个protected final的方法来改变state的值,分别是:getState、setState(int)、compareAndSetState(int, int)。根据修饰符,它们是不可以被子类重写的,但可以在子类中进行调用,这也就意味着子类可以根据自己的逻辑来决定如何使用state值。
综上,我们知道AQS有2个宝贝,一个等待队列,还有一个STATE标志,并且通过CAS来改变它的值。
AQS的子类应当被定义为内部类,作为内部的helper对象。事实上,这也是juc种锁的做法,如ReentrantLock,便是通过内部的Sync对象来继承AQS的。AQS中定义了一些未实现的方法(抛出UnsupportedOperationException异常)
tryAcquire(int) 尝试获取state
tryRelease(int) 尝试释放state
tryAcquireShared(int) 共享的方式尝试获取
tryReleaseShared(int) 共享的方式尝试释放
isHeldExclusively() 判断当前是否为独占锁
这些方法是子类需要实现的,可以选择实现其中的一部分。根据实现方式的不同,可以分为两种:独占锁和共享锁。其中JUC中锁的分类为:
独占锁:ReentrantLock、ReentrantReadWriteLock.WriteLock
共享锁:ReentrantReadWriteLock.ReadLock、CountDownLatch、CyclicBarrier、Semaphore
其实现方式为:
独占锁实现的是tryAcquire(int)、tryRelease(int)
共享锁实现的是tryAcquireShared(int)、tryReleaseShared(int)
AQS中还提供了一个内部类ConditionObject,它实现了Condition接口,可以用于await/signal。采用CLH队列的算法,唤醒当前线程的下一个节点对应的线程,而signalAll唤醒所有线程。
总的来说,AQS提供了三个功能:
实现独占锁
实现共享锁
实现Condition模型
综上对AQS的了解,我们大概已经可以把模板搭一下了。
public class MyLock implements Lock {
Helper helper = new Helper();
@Override
public void lock() {
helper.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
helper.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return helper.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return helper.tryAcquireNanos(1,unit.toNanos(time));
}
@Override
public void unlock() {
helper.release(1);
}
@Override
public Condition newCondition() {
return helper.newCondition();
}
private class Helper extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg){
}
@Override
protected boolean tryRelease(int arg) {
}
Condition newCondition() {
return new ConditionObject();
}
}
}
上面MYLOCK的大多数方法,其实我们根本不用操心,统一交给AQS的方法来帮我们完成就好。而我们真正要实现的就是tryAcquire和tryRelease 这2个操作。
首先我们还是结合前一章的思路来实现tryAcquire,本质就是如果第一个线程进来就拿到锁,后面进来的RETURN FALSE。然而之前我们声明了一个BOOL变量。这里我们可以直接AQS 的STATE变量。我们可以用初始值为0来表示没有线程拿到锁,一旦第一个线程进来了,就把它设置为非0.
protected boolean tryAcquire(int arg){
//如果第一个线程进来,拿到锁,返回TRUE
//如果第二个线程进来,返回FALSE,拿不到锁
int state = getState();
if(state == 0){
if(compareAndSetState(0,arg)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
}
return false;
}
下面我们再来思考释放,因为锁的获取和释放是一一对应的,我们首要判断锁住的线程是不是同一个,如果是,我们就把STATE - ARG。如果STATE回到0,则代表锁释放成功。
protected boolean tryRelease(int arg) {
//锁的获取和释放需要11对应,那么调用这个方法的线程,一定是当前线程让。
if(Thread.currentThread() != getExclusiveOwnerThread()){
throw new RuntimeException();
}
int state = getState() - arg;
setState(state);
if(state == 0){
setExclusiveOwnerThread(null);
return true;
}
return false;
}
下面我们就可以用之前的那个测试类Sequence来测试下我们写的锁了。
可以发现输出的是50,锁是成功的。
然后去测试重入锁的DEMO,发现会卡主。我们要再加一些逻辑来实现重入锁。
protected boolean tryAcquire(int arg){
//如果第一个线程进来,拿到锁,返回TRUE
//如果第二个线程进来,返回FALSE,拿不到锁
Thread t = Thread.currentThread();
int state = getState();
if(state == 0){
if(compareAndSetState(0,arg)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
}else if(getExclusiveOwnerThread() == t){
//因为只有一个线程可以进这个IF,所以没有线程安全性问题
setState(state+arg);
return true;
}
return false;
}
然后就实现好了,现在DEMO也能过了。