一、为什么要用AQS同步框架?
开发者如果不了解JMM和多线程编程,就会写出很多线程不安全的程序,即使是经验丰富的程序员,并发编程也难免会出错。
而对于java程序员来说,并发编程就变得容易得多了,因为并发编程大师Doug Lea为Java开发者提供了很多的并发容器和框架,而AQS就是java并发包下的一个核心框架。
AQS是抽象队列同步器,是用来构建Lock锁和同步组件的基础框架,JUC包下的很多锁和同步组件都是基于AQS构建的,比如ReentrantLock、ReentrantReadWriteLock、CountDownLatch、Semaphore。
为什么说AQS是构建Lock锁和同步组件的基础框架呢?
这里补充一点Lock和synchronized关键字的区别的知识:
- synchronized关键字是JVM层面上实现的,而Lock是JUC包下的一个接口
- synchronized让获取锁的线程执行完同步代码之后释放锁,线程执行发生异常的情况下,jvm会让线程释放锁,使用Lock的话,需要程序员显式地在finally块中释放锁,不然会造成死锁
- synchronized会让一直没获取到锁的线程阻塞等待,而Lock没有获取锁可以选择不用一直等待
- synchronized在发生异常的时候会自动释放占有的锁,不会出现死锁,Lock在发生异常的时候,不会主动释放占用的锁,必须手动unlock来释放锁,可能会引起死锁的发生
- synchronized无法判断锁的状态,而Lock可以判断
- synchronized是可重入锁,不响应中断,非公平的,而Lock是可重入锁,可判断,可公平的
- 在性能上,如果是并发量小的话,synchronized效率高,并发量高的话Lock高。Lock还可以提高多个线程读操作的效率(可以通过ReadWriteLock实现读写分离),ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步
设想一下,如果要实现线程的同步和锁机制,需要考虑哪些问题?
- 如何去获取锁和释放锁?
- 如何处理同步状态?
- 竞争失败时线程如何处理?
而上述问题,AQS已经帮你解决了:
- AQS实现了对同步状态的原子性管理
- AQS实现了对线程阻塞和解除阻塞的管理
- AQS实现了对同步队列的管理
Lock锁如何使用AQS?
AQS是实现锁的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。以ReentrantLock为例,它内部聚合了一个同步器Sync,这个同步器继承了AQS
如何使用AQS实现线程同步?
AQS定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器的主要使用方式是继承,子类通过继承AQS并实现它的抽象方法来管理同步状态,在抽象方法实现中免不了对同步状态进行更改,这时就需要使用同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们能够保证原子性的修改同步状态state字段。AQS既支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件。
二、什么是AQS同步框架?
AQS是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量state表示同步状态,通过内置的FIFO双向虚拟队列来完成资源获取线程的排队工作,并发包的作者Doug Lea期望AQS能够成为实现大部分同步需求的基础。
AQS使用的设计模式
AQS的设计是基于 模板方法模式 的,使用AQS需要继承它并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。
同步器可重写的方法有哪些?
在重写AQS指定的方法时,需要使用AQS提供的如下3个方法来访问或修改同步状态:
- getState():获取当前的同步状态
- setState(int newState):设置当前同步状态
- compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性(CAS机制实现)
AQS提供的模板方法有哪些?(部分)
AQS提供的模板方法基本分为3类:独占式获取与释放同步状态、共享式获取与释放同步状态和查询同步队列中的等待线程的情况。
三、AQS的底层原理(源码分析)
笔者打算从使用者的角度来分析AQS的底层,因此下面用AQS去实现一个简单的独占锁,独占锁就是在同一时刻只能有一个线程获取到锁,其他没有获取到锁的线程进入同步队列中等待。
用AQS实现一个简单的独占锁Mutex(代码来自AQS源码注释)
package com.demo.LockTest;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class Mutex implements Lock {
//静态内部类,自定义同步器
private static class Sync extends AbstractQueuedSynchronizer {
//是否被占用
protected boolean isHeldExclusively() {
return getState() == 1;
}
//当状态为0的时候获取锁
public boolean tryAcquire(int acquires) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
//释放锁,将状态设置为0
protected boolean tryRelease(int releases) {
//释放锁操作必须拥有锁,否则抛出异常
if (getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
//返回一个Condition,每个Condition都包含了一个condition队列
Condition newCondition() {
return new ConditionObject();
}
}
/**
* 然后仅仅需要将同步操作代理到Sync上
*/
private final Sync sync = new Sync();
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
sync.release(1);
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
public boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
}
上面的独占锁Mutex是一个自定义的同步组件,Mutex中定义了一个静态内部类,该内部类继承了AQS并实现了独占式获取和释放同步状态。在tryAcquire(int acquire)
方法中,使用CAS机制设置同步状态为1,并将当前线程标记为拥有锁的线程,否则进入同步队列中等待。tryRelease(int releases)
方法中只是将同步状态设置为0,然后将拥有锁的线程设置为null。
在Mutex的实现中,可以发现,用户使用这个类的时候不会直接和内部同步器交互(被设为private),而是通过调用Mutex提供的方法来使用独占锁。而Mutex的方法实现仅仅是去调用了同步器中的模板方法而已,这样就大大降低了开发一个可靠自定义同步组件的门槛
。
AQS是怎么实现线程同步的?
从实现的角度分析,线程同步主要包括:
- 同步队列
- 独占式同步状态的获取与释放
- 共享式同步状态的获取与释放
- 超时获取同步状态等同步器的核心数据结构与模板方法
1)同步队列
AQS依赖内部的同步队列来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息封装成一个结点(Node),并将其加入同步队列,同时阻塞当前线程,当同步状态释放时,会把首结点唤醒,使其再次尝试获取同步状态。
同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点。
Node prev
:前驱节点,当节点加入同步队列时被设置(尾部添加)Node next
:后继节点Thread waiter
:等待队列中的后继节点,若当前节点是共享的,那么这个字段将是一个SHARED常量,即节点类型(独占和共享)和等待队列中的后继节点共用同一个字段(?)。int status
:等待状态,包含CACELLED(1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)、INITIAL(0),后续会详细介绍这些状态
节点是构成同步队列(或等待队列)的基础,AQS拥有首节点(head)和尾节点(tail),没有成功获取同步状态的线程将会成为节点加入该队列的尾部,同步队列的基本结构如下所示:
注意看,AQS包含了两个节点类型的引用,一个指向head节点,一个指向tail节点。
AQS将节点加入到同步队列
首结点的设置
同步队列遵循FIFO(先进先出)的,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态的时候,会去唤醒后继节点,而后继节点在获取到同步状态时就会将自己设置为首结点,过程如下:
请注意,设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法不需要CAS来保证也是线程安全的,它只需要将首节点设置成为原首节点的后继节点并断开原首节点的next引用即可。
2)独占式同步状态的获取和释放
通过调用AQS的acquire(int arg)
方法可以获取同步状态,这个方法不响应中断,即线程获取同步状态失败后进入同步队列中,后续对该线程进行中断操作的时候,这个线程也不会从同步队列中移除。