AQS是一种设计的思想,通过一个锁变量(一般是int的变量),修饰为volatile保证各线程可见,获取锁相当于修改这个锁变量,一般修改通过CAS来实现,修改成功即持有锁,否则进入等待队列(等待队列是一个双向链表)的队尾
java里的Lock锁实现对象,其中控制锁资源的操作大部分都是基于AQS的思想,下面以Sync类来例子来介绍AQS的设计思想
进入ReentrantLock(可重入锁)的代码,可以看到他维护了一个Sync变量
ReentrantLock实现的阻塞/唤醒操作都是基于这个类的,下面我们从lock方法入手,一步步解析他是如何实现“锁操作”的(下述代码基于公平锁的实现)
Lock()
// ReentrantLock
public void lock() {
sync.lock();
}
// Sync
final void lock() {
acquire(1);
}
我们平常调用的lock方法实际都是调用acquire方法,传入的参数为1
acquire()
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire方法涉及到三个操作
- tryAcquire:尝试获取锁
- addWaiter:加入等待队列
- acquireQueued:后续操作
上面if条件逻辑为:如果获取锁失败,那么加入等待队列,并进行后续操作(阻塞并等待被唤醒),selfInterrupt是设置当前线程的一个中断操作(调用Thread.currentThread().interrupt())
tryAcquire()
tryAcquire方法是一个基于CAS的操作,通过尝试修改一个资源变量,来判断当前线程是否可以持有锁
首先需要判断当前锁资源状态是否为0(getState获取锁资源状态),如果为0说明锁处于释放状态,然后会尝试取获取锁(compareAndSetState是一个CAS操作,尝试把0修改为1)
注:这里可以看到有个hasQueuedPredecessors方法,这个是公平锁用来保证公平性的方法(非公平锁只有CAS操作)
修改锁资源变量成功后,会将当前线程设置到exclusiveOwnerThread变量中,后续用于判断是否可重入
如果资源变量不是0,那么会判断当前线程是否是占有锁资源的线程,如果是的话就将state+1(重入)
只要返回true就说明锁占有成功,否则失败
addWaiter()
如果获取锁失败就会进入addWaiter方法将当前线程包装为Node节点,这里传入的mode是Node.EXCLUSIVE独占模式,意思是只允许一个线程操作(另一种是共享模式,如CountDownLatch)
addWaiter方法会将线程包装为node节点并插入到队列的末端,这里的队列是一个虚拟队列,即节点通过pre和next指针来形成队列(CLH队列),并没有实际的队列实现类
如果队列已经存在节点,那么直接尝试插入队尾,如果插入失败,通过enq方法循环插入
enq也是一个初始化方法,当队列不存在节点会初始化一个Node(这个node没有任何含义,通过new出来的)
acquireQueued() - 阻塞
acquireQueued是对队列的一些操作,也可以理解为是从队列中取节点的操作,阻塞和唤醒都是在这个方法里完成的,所以里面是一个死循环。
这个方法会通过两个部分去介绍,分别对应阻塞和唤醒
这里我们先不去关注里面定义的局部变量,直接看循环体内,node.predecessor()是获取节点前继节点的方法,得到前继节点p后,获取判断该节点是否为头节点,如果是头节点那么会尝试去获取锁。
这里有个问题,**为什么要判断前继节点是不是头节点,而不是判断自己本身是不是头节点呢?**还记得enq方法里的初始化嘛,初始化的时候头节点是直接new出来的,并没有实际意义(也没有跟线程绑定),所以我们应该是从头节点的下一个节点开始操作。
这里我们假设获取锁失败,那么最后就会进入shouldParkAfterFailedAcquire方法,这个方法是用来确保当前的节点在阻塞后,能够被唤醒(通过设置前继节点p的状态waitStatus,这里不作赘述),确保该节点能够被唤醒后,就会调用我们的parkAndCheckInterrupt进行阻塞操作
LockSupport.park是将线程阻塞的操作,此时我们的线程就停在了这个方法的位置,让出cpu时间,进入了漫长的等待
unLock()
现在来看下释放锁的过程
// ReentrantLock
public void unlock() {
sync.release(1);
}
// sync
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
上述代码中,tryRelease是尝试释放锁,unparkSuccessor是唤醒我们阻塞的线程
tryRelease
这里会判断持有锁的线程是不是当前线程,如果是的话,就会将state减1,直到减为0,才释放锁,这就是可重入锁的特性(同时可以看到,如果不是持有该锁的线程调用该方法,那么就会抛异常)
unparkSuccessor
释放锁成功后,就会调用unparkSuccessor来唤醒我们的线程
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 可以看到操作的是头节点的下一个节点
Node s = node.next;
// 如果该节点waitStatus > 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);
}
这里涉及比较多状态量的判断,这里不作过多赘述
acquireQueued() - 唤醒
重新回到这个方法,这里我们被唤醒后,又会尝试取获取锁,假设这次我们获取到了锁
setHead方法会将当前节点设置为头节点,并将线程信息置空,所以每次操作都是取头节点的下一个节点(因为头节点相当于一个傀儡节点)
退到最外层后,如果acquireQueued返回是true,那么会调用selfInterrupt,这是设置当前线程的中断状态,只有当线程是因为中断被唤醒,acquireQueued才是返回true(涉及中断的知识读者自行查阅)
至此我们获取到lock锁,继续执行后续代码~