一、AQS 简介
AQS,就是
AbstractQueuedSynchronizer,在同步组件的实现中,AQS是核心部分,同步组件的实现者通过使用AQS提供的模板方法实现同步组件语义,AQS则实现了对同步状态的管理,以及对阻塞线程进行排队,等待通知等等一些底层的实现处理。AQS的核心也包括了这些方面:同步队列,独占式锁的获取和释放,共享锁的获取和释放以及可中断锁,超时等待锁获取这些特性的实现,而这些实际上则是AQS提供出来的模板方法,归纳整理如下:
AbstractQueuedSynchronizer 的独占式锁方法如下:
// 独占式获取同步状态,如果获取失败则插入同步队列进行等待;public final void acquire(int arg) // 与acquire方法相同,但在同步队列中进行等待的时候可以检测中断;public final void acquireInterruptibly(int arg)// 在acquireInterruptibly基础上增加了超时等待功能,在超时时间内没有获得同步状态返回false;public final boolean tryAcquireNanos(int arg, long nanosTimeout)// 释放同步状态,该方法会唤醒在同步队列中的下一个节点public final boolean release(int arg)
AbstractQueuedSynchronizer 的共享式锁方法如下:
// 共享式获取同步状态,与独占式的区别在于同一时刻有多个线程获取同步状态;public final void acquireShared(int arg)// 在acquireShared方法基础上增加了能响应中断的功能;public final void acquireSharedInterruptibly(int arg)// 在acquireSharedInterruptibly基础上增加了超时等待的功能;public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)// 共享式释放同步状态public final boolean releaseShared(int arg)
二、同步队列
先要了解一下AQS中的同步队列。
当共享资源被某个线程占有,其他请求该资源的线程就会被阻塞,从而进入同步队列。
就数据结构而言,队列的实现就是数组或链表的形式。AQS 中的同步队列是通过链表实现的。
在AQS 中有一个静态内部类:
static final class Node { //指示节点正在共享模式下等待的标记 static final Node SHARED = new Node(); //指示节点正在独占模式下等待的标记 static final Node EXCLUSIVE = null; //节点从同步队列中取消 static final int CANCELLED = 1; //后继节点的线程处于等待状态,如果当前节点释放同步状态会通知后继节点,使得后继节点的线程能够运行; static final int SIGNAL = -1; //当前节点进入等待队列中 static final int CONDITION = -2; //表示下一次共享式同步状态获取将会无条件传播下去 static final int PROPAGATE = -3; //节点状态 volatile int waitStatus; //当前节点(线程)的前驱节点 volatile Node prev; //当前节点(线程)的后继节点 volatile Node next; //加入同步队列的线程引用 volatile Thread thread; //等待队列中的下一个节点 Node nextWaiter; final boolean isShared() { return nextWaiter == SHARED; } final Node predecessor() throws NullPointerException { Node p = prev; if (p == null) throw new NullPointerException(); else return p; } Node() { } Node(Thread thread, Node mode) { // Used by addWaiter this.nextWaiter = mode; this.thread = thread; } Node(Thread thread, int waitStatus) { // Used by Condition this.waitStatus = waitStatus; this.thread = thread; }}
从上面可以看出来,AQS 的同步队列有前驱结点和后续节点,它是一个双向链表。下面看一个Demo
public class Test { public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(); MyThread myThread = new MyThread(lock); for (int i = 0; i < 5; i++) { new Thread(myThread).start(); } }} class MyThread implements Runnable { private final ReentrantLock lock; static int count; MyThread(ReentrantLock lock) { this.lock = lock; } @Override public void run() { lock.lock(); System.out.println("count == " + count++); lock.unlock(); }}
实例代码中开启了5个线程,先获取锁之后再睡眠10S中,实际上这里让线程睡眠是想模拟出当线程无法获取锁时进入同步队列的情况。通过debug,当Thread-4(在本例中最后一个线程)获取锁失败后进入同步时,AQS时现在的同步队列如图所示:
Thread-0先获得锁后进行睡眠,其他线程(Thread-1,Thread-2,Thread-3,Thread-4)获取锁失败进入同步队列,同时也可以很清楚的看出来每个节点有两个域:prev(前驱)和next(后继),并且每个节点用来保存获取同步状态失败的线程引用以及等待状态等信息。另外AQS中有两个重要的成员变量:
private transient volatile Node head;private transient volatile Node tail;
也就是说AQS实际上通过头尾指针来管理同步队列,同时实现包括获取锁失败的线程进行入队,释放锁时对同步队列中的线程进行通知等核心方法。其示意图如下:
通过对源码的理解以及做实验的方式,现在我们可以清楚的知道这样几点:
- 节点的数据结构,即AQS的静态内部类Node,节点的等待状态等信息;
- 同步队列是一个双向队列,AQS通过持有头尾指针管理同步队列;
那么,节点如何进行入队和出队是怎样做的了?实际上这对应着锁的获取和释放两个操作:获取锁失败进行入队操作,获取锁成功进行出队操作。
三、独占锁
1. 独占锁的获取
独占锁的获取(acquire方法)
继续通过看源码和debug的方式来看,还是以上面的demo为例,调用lock()方法是获取独占式锁,获取失败就将当前线程加入同步队列,成功则线程执行。而lock()方法实际上会调用AQS的acquire()方法,源码如下:
public final void acquire(int arg) { //先看同步状态是否获取成功,如果成功则方法结束返回 //若失败则先调用addWaiter()方法,再调用acquireQueued()方法 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();}
acquire根据当前获得同步状态成功与否做了两件事情:
- 成功,则方法结束返回
- 失败,则先调用addWaiter()然后在调用acquireQueued()方法。
1.1 获取同步状态失败,入队操作
当线程获取独占式锁失败后就会将当前线程加入同步队列,那么加入队列的方式是怎样的了?我们接下来就应该去研究一下addWaiter()和acquireQueued()。addWaiter() 源码如下:
private Node addWaiter(Node mode) { // 1. 将当前线程构建成Node类型 Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure // 2. 当前尾节点是否为null Node pred = tail; if (pred != null) { // 2.2 将当前节点尾插入的方式插入同步队列中 node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 2.1. 当前同步队列尾节点为null,说明当前线程是第一个加入同步队列进行等待的线程 enq(node); return node;}
分析可以看上面的注释。程序的逻辑主要分为两个部分:
- 当前同步队列的尾节点为null,调用方法enq()插入;
- 当前队列的尾节点不为null,则采用尾插入(compareAndSetTail() 方法)的方式入队。
另外还会有另外一个问题:如果 if (compareAndSetTail(pred, node))为false怎么办?会继续执行到enq()方法