AQS是Java中的一个并发编程的框架,通过这个框架实现了一些并发编程中实用的功能。
本篇只会简单的提一下AQS
的概念和使用方式,后续的笔记中会依据各个AQS
实现类的源码来更深入的解析。
AQS
全称AbstractQueuedSynchronizer
,顾名思义就是基于队列来实现的同步器框架,它可以实现线程之间的同步协作。我们经常会使用到的一些并发工具类都是基于它来实现的。
如重入锁ReentrantLock
、读写锁ReentrantReadWriteLock
、信号量SemaPhore
、线程池ThreadPoolExecutor
、计数器CountdownLatch
、回环屏障CyclicBarrier
。
AQS
中维护了一个共享资源state
状态字段和一个通过双向链表实现的FIFO队列。通过volatile
来保证state
和队列头尾节点的可见性,对state
的修改及队列的头尾节点的变更通过CAS
保证操作的原子性。
对于共享资源state
的使用有独占和共享两种方式,独占的情况下,只有一个线程能对state
做修改,共享则是所有线程都可以对state
做修改。
有时候也会两种方式结合使用,如ReentrantReadWriteLock
线程对写锁是独占的,对读锁是共享的,如果有一个线程持有了写锁,那其他的线程既不能获取到写锁也不能获取到读锁。
对于存在互斥的情况,就需要使用到FIFO队列了,没有抢占到共享资源的线程会进入到这个队列中等待,直到持有共享资源的线程运行结束时,才会唤醒队列头部的线程。
上面提到了AQS
是通过state
和队列实现的,并且通过CAS实现原子性操作,我们可以先看一下在这个类中是如何定义的:
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
// 共享资源sate
private volatile int state;
// 定义队列的头尾节点
private transient volatile Node head;
private transient volatile Node tail;
// CAS实现对state变量的原子性操作
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
// 尝试占用共享资源,失败则进入队列阻塞
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 尝试释放共享资源,成功则唤醒队列中阻塞的线程
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
}
上面的AQS
源码就是实现互斥锁的关键了,其中的tryAcquire()
和tryRelease()
是模板方法,由实现类去自行实现加锁和解锁的判断。
也就是说,我们自定义一个互斥锁的话,只需要新建一个类继承AbstractQueuedSynchronizer
然后重写这两个模板方法就可以了。
public class MyMutex extends AbstractQueuedSynchronizer {
public void lock() {
acquire(1);
}
public void unLock() {
release(1);
}
@Override
protected boolean tryAcquire(int arg) {
return compareAndSetState(0, 1);
}
@Override
protected boolean tryRelease(int arg) {
// 只可能有一个线程持有锁,释放锁时不需要CAS
setState(0);
return true;
}
}
几行代码就实现了一个最简单的互斥锁,但是这个简单的互斥锁没有实现可重入,容易导致死锁,如何实现可重入呢?
可重入实现的关键是在于加锁是判断是否是当前线程在调用加锁的方法,需要在加锁时保存当前线程以作为后续的判断标识。
在上面的代码中可以看到,AQS
还有一个父类AbstractOwnableSynchronizer
。
public abstract class AbstractOwnableSynchronizer
implements java.io.Serializable {
// 定义保存当前线程的变量
private transient Thread exclusiveOwnerThread;
// 设置当前线程
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
// 获取当前线程
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}
然后对MyMutex
中的模板方法的实现做一点修改:
@Override
protected boolean tryAcquire(int arg) {
Thread currentThread = Thread.currentThread();
// 抢占锁成功则设置当前线程为持有锁的线程
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(currentThread);
return true;
}
// 抢占不成功时,判断是否被当前线程自己给占用了
else if (currentThread == getExclusiveOwnerThread()) {
// 重入次数 +1,用于后续依次解锁
setState(getState() + arg);
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
// 只可能有一个线程持有锁,释放锁时不需要CAS
int state = getState();
// 依次减少重入次数,当state为0时解锁成功
int newState = state - arg;
boolean unLockSuccess = false;
if (newState == 0) {
// 清除当前持有的线程
setExclusiveOwnerThread(null);
unLockSuccess = true;
}
setState(newState);
return unLockSuccess;
}
以上就是重入锁的实现方式,只需要加入是否为当前线程的判断以及重入次数的正确增减就可以了。
在我们实际的使用中不需要自己再做互斥锁的封装,JUC包中提供了直接可以直接使用的锁工具类ReentrantLock
,提供了上述的自定义互斥锁的所有功能,而且还支持公平锁、条件判断功能。
本篇没有对AQS
中入队和出队的细节做分析,下一篇在分析ReentrantLock
源码的时候,会带入这部分细节的分析。