AbstractQueuedSynchronizer 是构建 Lock 锁或者其他同步组件的基础框架,使用一个Int变量来表示同步状态,通过内置的FIFO(first in first out)队列来完成共享资源的线程排队工作。—— 《Java并发编程的艺术》
我们了解一个东西,一般都是有很多疑问,然后逐个击破,所以这里我们就带着疑问出发,看看AQS到底是个什么东西呢?
问题1:AQS是个什么呢?AQS结构是怎么样的?
是什么?
AQS是个什么在开篇就提到,它和synchronized关键字一样,都是用来控制多线程情况下的共享资源的可见性问题。避免多个线程同时修改同一个对象,造成结果与预期不一致。synchronized关键字会隐式的获得锁及释放锁,而AQS因为获取锁、释放锁的操作是交给程序员去控制,所以控制性更强。
结构?
1.队列:1个同步队列,n等待队列。同步队列是用来排队争抢共享资源的多个线程的队列(SyncQueue);等待队列则是用来存放得到了锁,而主动放弃锁进入等待状态(waiterQueue)的线程。(类比Object.wait()方法,且等待队列可有 n 个,通过Lock接口的.newCondition()方法创建)。
队列的存放并不是直接放一个队列集合,而是记录了head(等待队列叫firstWaiter),tail(等待队列叫lastWaiter)两个首尾节点,在一般情况下并不需要遍历列表,所以效率很高。
结构如图:
2.state:一个volatile修饰的int型变量,用于标识同步状态。当state>0时,表示对象锁已被某线程成功获取。state=0,为可尝试获取。
3.内部类Node:用final修饰,无法继承、修改。主要作用是包装当前线程为节点,然后放入队列中。
4.内部类ConditionObject: 等待队列。继承自Condition接口,在Lock.newCondition()中返回实例,可创建多个。
问题2:AQS为什么可以做到同步效果?
这个问题我们得想想同步效果是个什么效果?多个线程竞争一个资源,在同一时刻只有一个能取得控制权,而没取到的那些家伙怎么办,去哪里了呢。在同步队列里排着队,蓄势待发,等着抢下一轮的争夺(cpu调度)。
我们可以从源码来追踪看看。找到入口:加锁的方法Lock.lock,所以我们找到它的实现类ReentrentLock。从ReentrentLock.lock()进入,看看它的子类NoFairSync(非公平锁)的实现。
当compareAndSetState方法去获取锁失败时,进入acquire(1),1是自定义的数字,当(state=1)>0则表示获取了锁。compareAndSetState是常说的CAS操作,有什么用呢?就是保证设置状态时操作是原子的。继续看看后续:
其中tryAcquire(arg)是抽象方法,由实现类去实现,表示尝试是否可以获取锁(判断state是否等于0),不行进入acquireQueued(addWaiter(Node.EXCLUSIVE), arg)。其中addWaiter是将当前线程包装为节点Node放入同步队列尾部,然后调用acquireQueued(final Node node, int arg),循环去获取对象锁,失败就阻塞,被唤醒又争夺,失败又阻塞,直到被中断或拿到锁。源码如下:
那么看看如何阻塞的?
就是它!LockSupport.park(this); LockSupport是concurrent包中提供的基础工具类,用于阻塞线程。而真正阻塞线程的是一个unsafe.park()的方法。
unsafe是用于执行低级别、不安全操作的方法集合。尽管这个类和所有的方法都是公开的(public),但是这个类的使用仍然受限,你无法在自己的java程序中直接使用该类,因为只有授信的代码才能获得该类的实例。一般我们无法拿到这个类的实例。
所以到这里,就知道为什么AQS可以阻塞线程啦。而等待队列中同样是用LockSupport.park()来阻塞线程。
问题3:阻塞线程什么时候唤醒?唤醒之后会执行什么操作?
我们在使用Lock时,常使用Lock lock = ReentrentLock(可重入锁)。获取锁为lock.lock(),而释放锁时则调用lock.unlock();假设现在有三个线程A、B、C同时在竞争,且A之前已经获取了锁,准备释放。
当A需要释放锁时,调用lock.unlock(),因此我们从unlock()方法着手,看看源码怎么实现唤醒,已经做了哪些操作。
这里tryReleases()为ReentrentLock的实现,因为是可重入的,所以state不一定为1,可以是2,3,4...。当c等于0时,清空线程信息。注意,若不是当前获得锁的线程调用是会抛出IllegalMonitorStateException()的。
如果头结点head不为空,且未被取消(h.waitStatus!=0),则唤醒下一个节点,调用unparkSuccessor(h)。这里B线程、C线程均在队列中等待,若B线程为head,则此处会被唤醒。看一下unparkSuccessor的实现:
从中可以看到如果有下一个节点,则找到后唤醒。
线程唤醒后,回到之前阻塞的地方:ParkAndCheckInterrupt(),检查是否线程被中断。未被中断则进入循环,重新进行锁竞争。若被中断,则调用selfInterrupt();
问题4:ConditionObject 里的队列什么时候唤醒 / 阻塞?
何时阻塞?
lock通过调用方法newCondtion()返回一个condition实例。每个实例中都有一个waiterQueue。当在有需要时(比如阻塞队列满了,得等空了才能放入),我们会创建conditon对象,调用condition.await()方法(类比Object.wait() )来放弃锁,进入等待队列。
何时唤醒?
同Object调用notify()相似,Lock调用signal()来唤醒阻塞在等待队列的线程。
为了方便理解过程,用一个简单的固定大小的阻塞队列实现来观察:
public class FixSizeBlockingQueue {
private Lock lock = new ReentrantLock();//默认非公平锁
private LinkedList msgQueue = new LinkedList<>();//消息队列
private static final int MAXSIZE = 3;//最大消息队列容量
private volatile int count;
private volatile int total;
//队列满了,则用它阻塞线程
private Condition fullCod = lock.newCondition();
//队列空了,用它阻塞线程
private Condition nullCod = lock.newCondition();
//添加一条消息
public boolean add(String msg){
boolean result = false;
lock.lock();
try{
if(count>=MAXSIZE){
fullCod.await();//队列满了,阻塞
}
result = msgQueue.add(msg);
if(result){
count++;
total++;
nullCod.signal();//唤醒因队列为空,取不到数据而阻塞的队列
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return result;
}
//获取一条消息
public String get(){
String result = null;
lock.lock();
try{
if(count==0){
nullCod.await();//队列空了,阻塞
}
result = msgQueue.poll();
if(result!=null){
count--;
fullCod.signal();//唤醒因队列满了而阻塞的线程
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return result;
}
public int totalNum(){
return total;
}
}
测试类:
class FixSizeBlockingQueueTest {
static FixSizeBlockingQueue queue;
@BeforeAll
public static void init(){
queue = new FixSizeBlockingQueue();
}
@Test
public void test() throws InterruptedException {
//2个生产者
for(int i=0;i<2;i++){
String name = "creator-"+i;
Creater creater = new Creater(name,queue);
Thread thread = new Thread(creater);
thread.start();
}
//2个消费者
for(int i=0;i<2;i++){
String name = "cust-"+i;
Cunstom cunstom = new Cunstom(name,queue);
Thread thread = new Thread(cunstom);
thread.start();
}
Thread.sleep(20000);
}
//生产者
public class Creater implements Runnable{
private String no;//编号
private FixSizeBlockingQueue queue;
public Creater(String no, FixSizeBlockingQueue queue) {
this.no = no;
this.queue = queue;
}
@Override
public void run() {
int i=0;
while (queue.totalNum()<20){
String msg = "生产者("+no+")生成了一条消息:msg"+i;
long begin = System.currentTimeMillis();
System.out.println(begin+"\r\r"+msg);
queue.add(msg);
i++;
System.out.println("生产者("+no+")生成的消息放入成功,耗时:"+(System.currentTimeMillis()-begin));
}
}
}
//消费者
public class Cunstom implements Runnable{
private String no;//编号
private FixSizeBlockingQueue queue;
public Cunstom(String no, FixSizeBlockingQueue queue) {
this.no = no;
this.queue = queue;
}
@Override
public void run() {
while(queue.totalNum()<20){
long begin = System.currentTimeMillis();
System.out.println("消费者者("+no+")开始读取消息。");
String msg = queue.get();
System.out.println("消费者者("+no+")消费到一条消息,耗时:"+(System.currentTimeMillis()-begin)+" 内容为:"+msg);
}
}
}
}
在这个例子中,可以看到ConditionObject是有两个的,一个fullCod,一个NullCod,线程被谁调用就在谁的等待队列中排队。
最后,同步器中子类Node中的waitState状态变化,如singal,cancelled,condition等,在控制场景中也是变化较多,不过就不去慢慢罗列了,在锁控制的流程上,跟着源码走一遍,相信就了解了。