ReentrantLock
ReentrantLock的实现依赖于AQS。
首先 ReentrantLock 中有一个内部类sync,sync继承了AQS并实现了里面方法(看源码感觉更多的是调用),然后又有公平锁和非公平锁共同继承了sync来实现方法。其他方法的调用就是创建公平/非公平锁对象再继续实现,所以AQS是ReentrantLock的基础。
sync定义
abstract static class Sync extends AbstractQueuedSynchronizer
非公平锁
static final class NonfairSync extends Sync
公平锁
static final class FairSync extends Sync
AQS
是什么
AQS是AbstractQueuedSynchronizer的简称,即抽象队列同步器,从字面意思上理解:
- 抽象:抽象类,只实现一些主要逻辑,有些方法由子类实现;
- 队列:使用先进先出(FIFO)队列存储数据;
- 同步:实现了同步的功能。
有什么用
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的同步器,比如我们提到的ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。
我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器,只要之类实现它的几个protected方法就可以了。
结构及概念
AQS核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中。CLH:Craig、Landin and Hagersten队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。
而AQS类本身实现的是一些排队和阻塞的机制,比如具体线程等待队列的维护(如获取资源失败入队/唤醒出队等)。它内部使用了一个先进先出(FIFO)的双端队列,并使用了两个指针head和tail用于标识队列的头部和尾部。
节点
节点/node 是AQS中的基础,线程也是存储在节点中的。
static final class Node {
// 标记一个结点(对应的线程)在共享模式下等待
static final Node SHARED = new Node();
// 标记一个结点(对应的线程)在独占模式下等待
static final Node EXCLUSIVE = null;
// waitStatus的值,表示该结点(对应的线程)已被取消
static final int CANCELLED = 1;
// waitStatus的值,表示后继结点(对应的线程)需要被唤醒
static final int SIGNAL = -1;
// waitStatus的值,表示该结点(对应的线程)在等待某一条件
static final int CONDITION = -2;
/*waitStatus的值,表示有资源可用,新head结点需要继续唤醒后继结点(共享模式下,多线程并发释放资源,而head唤醒其后继结点后,需要把多出来的资源留给后面的结点;设置新的head结点时,会继续唤醒其后继结点)*/
static final int PROPAGATE = -3;
// 等待状态,取值范围,-3,-2,-1,0,1
volatile int waitStatus;
volatile Node prev; // 前驱结点
volatile Node next; // 后继结点
volatile Thread thread; // 结点对应的线程
Node nextWaiter; // 等待队列里下一个等待条件的结点
// 判断共享模式的方法
final boolean isShared() {
return nextWaiter == SHARED;
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
//返回前驱节点,没有则返回异常
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
// 其它方法忽略,可以参考具体的源码
}
// AQS里面的addWaiter私有方法,不在node结构中
private Node addWaiter(Node mode) {
// 使用了Node的这个构造函数
Node node = new Node(Thread.currentThread(), mode);
// 其它代码省略
}
资源有两种共享模式,或者说两种同步方式/两种锁:
- 独占模式(Exclusive):资源是独占的,一次只能一个线程获取。如ReentrantLock。
- 共享模式(Share):同时可以被多个线程获取,具体的资源个数可以通过参数指定。如Semaphore/CountDownLatch。
实现
AQS内部使用了一个volatile的变量state来作为资源的标识。同时定义了几个获取和改版state的protected方法,子类可以覆盖这些方法来实现自己的逻辑:
private volatile int state;
//获取state
protected final int getState()
//设置state
protected final void setState(int newState)
//通过cas操作设置```
protected final boolean compareAndSetState(int expect, int update)
//都是原子操作
上面的三种方法都由final修饰,这说明子类中无法重写它们。
我们通过修改state的值来实现共享模式和独占模式的实现。
独占:state初始设置为0,获得锁则为1。尝试获取锁就是判断是否为0。
共享:state初始设置为某值,获取锁时判断是否为0,大于0通过cas自减,为0则说明不能再共享了。
源码
AQS的设计是基于模板方法模式的,前面提到了,我们自己也可以通过继承AQS实现我们自己的同步器,子类要去实现一部分方法,主要有:
上面的方法都被protected修饰,在AQS中使用都是抛出异常
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}
下面通过获取资源和释放资源两部分来说明一些源代码
ReentrantLock中非公平锁中的加锁操作:
final void lock() {
//设置state成功,将当前线程设置为资源拥有线程
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//否则执行下面方法,调用的AQS的方法
acquire(1);
}
在AQS中acquire是这样的
public final void acquire(int arg) {
//先调用TryAcquire,失败就继续执行acquireQueued中的addWaiter方法,成功后执行selfInterrupt方法
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire在AQS中是抛出异常,所以该方法是在子类中定义好的,是使用的ReentrantLock的方法。这里就不说了。
如果tryAcquire成功则说明资源获取成功,后续不再执行。
失败就要先执行addWaiter方法,从名字就知道是将线程添加到等待队列(调用的AQS中的方法,RL中没有重写该方法)addWaiter是一个在双端链表添加尾节点的操作,需要注意的是,双端链表的头结点是一个无参构造函数的头结点。
private Node addWaiter(Node mode) {
//将当前线程放入新节点中
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
(1)通过当前的线程和锁模式新建一个节点。
(2)Pred指针指向尾节点Tail。
(3)将New中Node的Prev指针指向Pred。
(4)通过compareAndSetTail方法,完成尾节点的设置。这个方法主要是对tailOffset和Expect进行比较,如果tailOffset的Node和Expect的Node地址是相同的,那么设置Tail的值为Update的值。
(5) 如果Pred指针是Null(说明等待队列中没有元素),或者当前Pred指针和Tail指向的位置不同(说明被别的线程已经修改)。
就是说cas操作成功就返回节点,失败就向后执行。
自旋CAS插入等待队列
private Node enq(final Node node) {
for (;;) {
//获取头结点
Node t = tail;
//没有头结点就创建一个
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
回到acquireQueued方法,其源码如下:
final boolean acquireQueued(final Node node, int arg) {
// 标记是否成功拿到资源
boolean failed = true;
try {
// 标记等待过程中是否中断过
boolean interrupted = false;
// 开始自旋,要么获取锁,要么中断
for (;;) {
// 获取当前节点的前驱节点
final Node p = node.predecessor();
// 如果p是头结点,说明当前节点在真实数据队列的首部,就尝试获取锁(别忘了头结点是虚节点)
if (p == head && tryAcquire(arg)) {
// 获取锁成功,头指针移动到当前node
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 说明p为头节点且当前没有获取到锁(可能是非公平锁被抢占了)或者是p不为头结点,这个时候就要判断当前node是否要被阻塞(被阻塞条件:前驱节点的waitStatus为-1),防止无限循环浪费资源。具体两个方法下面细细分析
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
流程图如下:
释放资源/锁 ReentrantLock在解锁的时候,并不区分公平锁和非公平锁。
public void unlock() {
sync.release(1);
}
AQS中的relase
public final boolean release(int arg) {
// 上边自定义的tryRelease如果返回true,说明该锁没有被任何线程持有
//tryrelase由reentrantLock实现
if (tryRelease(arg)) {
// 获取头结点
Node h = head;
// 头结点不为空并且头结点的waitStatus不是初始化节点情况,解除线程挂起状态
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
// 如果状态是负数,尝试把它设置为0
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 得到头结点的后继结点head.next
Node s = node.next;
// 如果这个后继结点为空或者状态大于0
// 通过前面的定义我们知道,大于0只有一种可能,就是这个结点已被取消
if (s == null || s.waitStatus > 0) {
s = null;
// 等待队列中所有还有用的结点,都向前移动
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 如果后继结点不为空,
if (s != null)
LockSupport.unpark(s.thread);
}