AQS简介
AQS是一个抽象的队列同步器,他为锁和一些同步组件如(Semaphores,CountDownLatch)提供了一个框架,AQS依赖于内部的一个FIFO的双向队列,同步器会将当前线程和其等待状态包装成一个Node添加到队列中, AQS提供了3个方法来更改同步状态:getState(),setState(int state),compareAndSetState(int expect,int update)
通过ReentrantLock中非公平锁的实现来具体看看AQS是怎么为我们提供同步的能力的
ReentranLock有一个内部静态抽象类NonfairSync继承了Sync,Sync又继承了我们的AbstractQueuedSynchronizer,我们从加锁到解锁一步步看看整个流程是怎么样的
加锁
public void lock() {
sync.lock();
}
这里是ReentrantLock提供的lock方法,当我们选择非公平锁的时候,这里的sync实现类就是NonfairSync,所以这里会调用NonfairSync的lock方法
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
if (compareAndSetState(0, 1))//①
setExclusiveOwnerThread(Thread.currentThread());//②
else
//
acquire(1);//③
}
}
①:原子的去设置state的值,如果能设置成功就代表获取到了锁(state初始值为0,这里设置为1,也就是说state=0的时候没有线程占有锁,state=1的时候说明锁被其他线程获取了)
②:设置当前线程为独占锁的拥有者,主要是为了可重入
③:如果原子设置state值失败了就要调用acquire尝试去获取锁,这里的acquire会调用到父类AbstractQueuedSynchronizer中的的acquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg)①
&&
acquireQueued(addWaiter(Node.EXCLUSIVE)②, arg))③
selfInterrupt();
}
①:调用tryAcquire去尝试的获取锁,tryAcquire是AQS提供的一个模板方法,需要子类去实现,在NonfairSync的实现为:
protected final boolean tryAcquire(int acquires) {
//会调用带父类Sync中的nonfairTryAcquire
return nonfairTryAcquire(acquires);//acquires=1
}
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
int c = getState();
//获取当前state的值
if (c == 0) {
//如果是0,说明此时没有人占有锁,就可以尝试去占有锁了(设置state=1)
if (compareAndSetState(0, acquires)) {
//设置成功了就可以设置当前线程是独占锁拥有者了
setExclusiveOwnerThread(current);
return true;
}
}
//如果c!=0,但是当前线程又是锁的拥有者
else if (current == getExclusiveOwnerThread()) {
//把获取锁的次数+1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//这里就不需要原子的去设置了,因为没有其他线程能够拿到锁,这里是线程安全的
setState(nextc);
return true;
}
//没能尝试获取到锁,返回false之后会被添加带AQS的队列里等待
return false;
}
②:当tryAcquire返回false即没有获取到锁,那么这里就要调用addWaiter将当前线程包装成Node添加到AQS的等待队列中去
//mode是Node.EXCLUSIVE即独占式的
private Node addWaiter(Node mode) {
//用是Node.EXCLUSIVE构造一个node节点出来
Node node = new Node(Thread.currentThread(), mode);
//尝试快速的将节点添加带队尾,个人理解之所以为快速就是说这里的争抢不太激烈的情况下能很快的设置节点到队尾,如果争抢比较激烈就需要进入enq方法里不断自旋的去将这些节点设置到队尾
Node pred = tail;
//这里要判断下队尾是不是null,是null的话这里不处理
if (pred != null) {
//将包装好的节点的前驱设置为tail
node.prev = pred;
//原子的去设置tail,设置成功就将tail的后置节点设置为当前的node并返回
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//进入自旋方法设置队尾
enq(node);
return node;
}
private Node enq(final Node node) {
//进入到这个方法说明有多个线程此时需要添加到队尾
//这个方法就是不断自旋的去设置队尾节点,最终将竞争的节点全部依次添加到队尾
for (;;) {
Node t = tail;
if (t == null) { // 队列需要初始化
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
③:调用acquireQueued方法开始自旋的去获取锁,其中会尝试去设置前一个节点的状态为SINGAL(shouldParkAfterFailedAcquire方法),设置好了自己就可以进入阻塞状态(parkAndCheckInterrupt方法)等待前一个节点的解锁通知或者中断信号了
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
//中断标识符
boolean interrupted = false;
for (;;) {
//获取当前节点的前驱
final Node p = node.predecessor();
//如果前驱是头节点,那么当前线程就可以尝试去获取锁了,当前线程可能是是被头节点唤醒的
if (p == head && tryAcquire(arg)) {
//获取锁成功,设置当前线程是头节点
setHead(node);
//将p的后置节点设置为null,在setHead方法中已经将node的前驱节点设置为null,这样就相当于原来的head节点已经从队列中出队了,没有引用了就方便GC
p.next = null;
failed = false;
return interrupted;
}
//这里要去判断当前线程能不能阻塞等待唤醒
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//pred是头节点,node是当前节点,这里先获取头节点的等待状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//如果前驱的状态是SIGNAL就说明当前线程已经通知过前驱如果释放了锁要通知自己醒来,所以就可以阻塞等待唤醒了
return true;
if (ws > 0) {
//前驱节点的状态如果是>0的说明前驱已经放弃获取锁了(CANCELLED),那么就顺着前驱往前找,直到找到一个状态不是CANCELLED的节点,然后排在他后面
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//这里就设置前驱的状态为SIGNAL,这样后面再次判断自己是否可以休眠的时候就可以直接休眠了,当然这里也可能失败,失败了就下次再尝试
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
//让当前线程阻塞
LockSupport.park(this);
//这里就是被中断了,返回中断状态
return Thread.interrupted();
}
到这里加锁的流程基本就结束了,来总结一下流程:
①:通过非公平的方式调用ReentrantLock的lock()方法,会直接尝试更改state的值为1即尝试加锁操作(这里体现了非公平,因为每个线程进来都可以去尝试获取锁,而不需要排队,失败了才去排队,公平锁的方式就没有这一步),如果成功了,则设置当前线程为锁的独占线程
②:①中如果获取失败了,那么就要在去尝试获取锁(tryAcquire方法),这个逻辑里会判断是否是重入,如果获取成功了就不做任何操作
③:如果②也获取失败了,就要把自己构造成一个Node添加到AQS的队列中去
④:之后当前线程会进入到一个自旋操作中去,当自己的前一个线程是头结点时才会去尝试获取锁,其他情况会进入到阻塞状态中去等待前一个节点的通知或者中断信号
解锁
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
//tryRelease是一个模板方法,具体实现是在ReentrantLock的Sync中
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
//用state - 1
int c = getState() - releases;
//如果尝试解锁的线程不是独占线程直接抛异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
//c==0说明重入锁已经释放完毕了,可以真正解锁了,返回true让上面的方法进入到if block中去执行唤醒后置节点去尝试获取锁
free = true;
setExclusiveOwnerThread(null);
}
//如果释放完毕了,就设置state值为0,返回true,否则设置当前重入次数,返回false
setState(c);
return free;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
//这里如果s的状态如果是CANCELD,那么就需要从尾部往前找,找到队列最前边那个状态不是CANCELD的节点
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);
}
总结一下整个解锁的步骤:
①:针对重入锁进行解锁操作(设置state - 1),如果state - 1 = 0,说明重入次数已经释放完毕,就可以解锁了,否则就只是将重入次数-1
②:唤醒后继节点去尝试获取锁,这里唤醒的下一个节点如果状态是CANCELD,那么就要从队尾一直向前找,找到最前面那个状态不为CANCELD的节点并唤醒他,让他去尝试获取锁
上述过程做完就算是解锁完毕了
总结
ReentrantLock的加锁和解锁主要就是依赖于AQS提供出来的几个模板方法,其他的锁如ReentrantReadWriteLock和同步组件如Semaphores、CountDownLatch也是和ReentrantLock加解锁的流程也基本一致,都是依赖于AQS提供出来的模板方法,所以为什么说AQS是concurrent包下面的大多数锁和同步组件的依赖的基础组件,基本上弄懂了ReentrantLock的流程,其他组件的流程也就迎刃而解了
如果上面有什么错误的地方,欢迎大家指出来讨论。