目录
1.引入
什么是AQS
说到AQS(AbstractQueueSynchronizer),很多同学都表示没听过。但是重入锁,读写锁这些同步工具大家肯定都用过,AQS都是这些锁底层实现同步不可或缺的一个组件。
下面就来聊聊AQS
2.AQS的架构设计
AQS可以做什么
首先AQS是个抽象类,本身不能实例化,需要使用者根据实际情况去继承它。
AQS的主要功能是提供线程同步,通俗来说就是可以控制多个线程,让它们阻塞或者唤醒。通过AQS,我们能够实现像重入锁那样单个线程访问临界区代码,让其他线程等待;也可以实现像读写锁那样控制多个读线程同时访问资源。
AQS中主要包含什么
AQS中首先有一个Node内部类,用来封装线程,并且通过一个head节点和tail节点来形成Node链表,实现同步线程队列。还有一个整型变量state,用来表示锁的状态(这里说的不单单是锁,也可以用来表示其他其他互斥资源),进入AQS通过判断state的值来进行相应的操作(获取锁或者进入队列等待)。还有多个关键方法(acquire等),这些方法暴露出去,实现加锁,解锁的功能。还有一个ConditionObject(下文用Condition代替)对象,这个对象是用来实现类似wait/nodify机制的。但是通过AQS设计的锁更加高端,可以创建多个Condition对象,实现多组等待唤醒控制,而传统的synchronized只能通过wait/notify方法实现一组。
AQS的工作原理
AQS主要实现的功能是让一个或多个线程能正确获取锁(或资源)。例如同一时间只有一个线程获得锁,其他线程都等待该线程释放锁。
如上图所示,AQS用一个链表存储要获取锁的线程。每次外来线程想要获取锁时,都通过线程控制器进入同步线程队列。这个线程控制器是我抽象出来的,如果查看源码的话就能发现进入AQS的线程其实是进入了一个自旋状态,每次自旋通过CAS操作去修改state的值,如果修改成功就获得了锁。
比方说state的值为0时表示锁可用,那么CAS操作把他置为1,然后获得锁。这保证同一时间只有指定数量的线程能获取到锁。
后续进来的线程在自旋中并不能通过CAS操作把state的值从0变为1,那么就说明现在锁已被占用,那么这个线程就在同步线程队列中阻塞等待。
一般来说能拿到锁的都是头节点中的线程(非Head节点的第一个节点,接下来的头节点指的都是它)。那么当头节点线程释放锁时会唤醒它的后继节点,然后后继节点又进行一次自旋, 看看自己是不是头节点的next的节点,能不能通过CAS操作修改state的值,如果能就把头节点出队,自己成为头节点。
Condition的工作原理
使用wait/notify方法的前提是获得锁,使用Condition对象也采用了一样的设计。获得锁时,调用await方法能让当前线程释放锁,进入等待状态,其他获取锁的线程调用signal方法能唤醒调用同一Condition对象await方法进行等待的线程。
这整个过程其实AQS的工作原理很像,Condition内部也有一个链表实现的等待队列,如果某个获取锁的线程调用await的方法,就把这个线程从AQS的同步线程队列中出队,包装放入Condition对象的等待队列。然后其他获得锁的线程调用该Condition对象的signal方法时就把等待队列头节点出队,放到AQS同步线程队列的队尾去等待获取锁。
Condition和AQS操作链表的方式不同的地方在于Condition操作自己的等待队列时不用CAS的操作,因为操作等待队列的线程都是获取到了锁的线程,因此不存在竞争。但是AQS就不同了,比方说同时有多个线程要进入同步线程队列,那么如果不用CAS操作就会发生混乱。
3.AQS的应用
使用AQS的好处
在实际开发中,我们很少会直接去使用AQS,但是却很常用重入锁和读写锁,尤其是重入锁,在某些情况下会比传统的synchronized效率更高,这就得益于它内部的AQS,AQS用自旋CAS的方式减少了线程阻塞带来的开销。
而且重入锁还支持有限等待,公平获取锁,多条件等待唤醒等等的功能,这是synchronized所不具备的。
自己实现AQS
有些时候还真得自己实现一个AQS来实现一些功能。虽然AQS是个抽象类,但他没有任何抽象方法。我们使用它只需要通过继承覆写方法实现我们具体的需求即可。
下面结合重入锁对AQS的实现来聊聊怎么实现AQS。
操作同步线程队列的逻辑AQS都已经写好了,在这一块不需要做修改。主要重写的部分是实现资源获取逻辑。
还是用重入锁举例子,重入锁的功能是保证同一时刻只有一个线程可以访问临界区代码,也就是只有一个线程能获得锁,来看看它获取锁的代码。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//调用了AQS的acquire方法,传入1作为参数
acquire(1);
}
public final void acquire(int arg) {
//tryAcquire尝试去修改state状态,如果成功就继续执行代码。
if (!tryAcquire(arg) &&
//不成功就在acquireQueued方法中进入同步线程队列
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
//尝试以不公平的方式获取锁
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
//获得当前线程
final Thread current = Thread.currentThread();
int c = getState();//获得state状态
if (c == 0) {
//如果state的值为0,表示锁可用,这时用CAS操作把他修改1,如果成功就成功获得锁。
if (compareAndSetState(0, acquires)) {
//记录获得锁的线程,为以后该线程重新获取锁做准备
setExclusiveOwnerThread(current);
return true;
}
}
//如果state的值不是0,说明有其他线程已经获取锁,这是查看一下获得锁的线程是不是本线程
else if (current == getExclusiveOwnerThread()) {
//如果是就修改state的值
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//没有获得锁,返回false
return false;
}
重入锁主要是重写了tryAcquire方法(还有对应的释放锁方法tryRelease),这两个个方法在AQS中是直接抛出UnSupportException异常的。这个方法就是操作锁的逻辑,如果我们要根据自己的业务需求自己实现AQS,首选的就是重写这一套方法。
AQS还有一套操作锁的方法tryAcquireShared和tryReleaseShared,这个主要是用来实现多线程同时获取锁的。比如读写锁中的读锁就是用这一套方法设计,读锁允许同时多个读线程获得锁。