我们都知道ReentranLock
的加锁行为和Synchronized
类似,都是可重入的锁,但是二者的实现方式确实完全不同的,我们之后也会讲解Synchronized
的原理.除此之外,Synchronized的阻塞无法被中断,而ReentrantLock则提供了可中断的阻塞下面的代码是ReentranLock
的相关API,我们就以此为顺序,依次讲解.
1 |
ReentrantLock lock = new ReentrantLock(); |
ReentranLock分为公平锁和非公平锁.二者的区别就在获取锁是否和排队顺序相关.我们都知道,如果当前锁被另一个线程持有,那么当前申请锁的线程会被挂起等待,然后加入一个等待队列里.理论上,先调用lock
函数被挂起等待的线程应该排在等待队列的前端,后调用的就排在后边.如果此时,锁被释放,需要通知等待线程再次尝试获取锁,公平锁会让最先进入队列的线程获得锁,而非公平锁则会唤醒所有线程,让它们再次尝试获取锁,所以可能会导致后来的线程先获得了锁,则就是非公平.
1 |
public ReentrantLock(boolean fair) { |
我们会发现FairSync
和NonfairSync
都继承了Sync
类,而Sync
的父类就是AbstractQueuedSynchronizer
.但是AQS
的构造函数是空的,并没有任何操作.
之后的源码分析,如果没有特别说明,就是指公平锁.
ReentranLock
的lock
函数如下所示,直接调用了sync
的lock
函数.也就是调用了FairSync
的lock
函数.
1 |
//ReentranLock |
好,我们接下来就正式开始AQS
相关的源码分析了,acquire
函数你可以将其理解为获取一个同一时间只能有一个函数获取的量,这个量就是锁概念的抽象化.我们先分析代码,你慢慢就会明白其中的含义.
1 |
public final void acquire(int arg) { |
tryAcquire
,addWaiter
和acquireQueued
都是十分重要的函数,我们接下来依次学习一下这些函数,理解它们的作用.
1 |
//AQS类中的变量. |
由上述代码我们可以发现,tryAcquire
就是尝试获取那个线程独占的变量state
.state的值表示其状态:如果是0,那么当前还没有线程独占此变量;否在就是已经有线程独占了这个变量,也就是代表已经有线程获得了锁.但是这个时候要再进行一次判断,看是否是当前线程自己获得的这个锁,如果是,那么就增加state的值.
ReentranLock获得锁
这里有几点需要说明一下,首先是compareAndSetState
函数,这是使用CAS操作来设置state
的值,而且state值设置了volatile
修饰符,通过这两点来确保修改state的值不会出现多线程问题.然后是公平锁和非公平锁的区别问题,在UnfairSync
的nonfairTryAcquire
函数中不会在相同的位置上调用hasQueuedPredecessors
来判断当前是否已经有线程在排队等待获得锁.
如果tryAcquire
返回true
,那么就是获取锁成功,如果返回false,那么就是未获得锁.需要加入阻塞等待队列.我们下面就来看一下addWaiter
的相关操作
将保存当前线程信息的节点加入到等待队列的相关函数中涉及到了无锁队列的相关算法,由于在AQS
中只是将节点添加到队尾,使用到的无锁算法也相对简单.真正的无锁队列的算法我们等到分析ConcurrentSkippedListMap
时在进行讲解.
1 |
private Node addWaiter(Node mode) { |
通过调用addWaiter
函数,AQS
将当前线程加入到了等待队列,但是还没有阻塞当前线程的执行,接下来我们就来分析一下acquireQueued
函数.
由于进入阻塞状态的操作会降低执行效率,所以,AQS
会尽力避免试图获取独占性变量的线程进入阻塞状态.所以,当线程加入等待队列之后,acquireQueued
会执行一个for循环,每次都判断当前节点是否应该获得这个变量(在队首了),如果不应该获取或在再次尝试获取失败,那么就调用shouldParkAfterFailedAcquire
判断是否应该进入阻塞状态,如果当前节点之前的节点已经进入阻塞状态了,那么就可以判定当前节点不可能获取到锁,为了防止CPU不停的执行for循环,消耗CPU资源,调用parkAndCheckInterrupt
函数来进入阻塞状态.
1 |
final boolean acquireQueued(final Node node, int arg) { |
由上述分析,我们知道了AQS
通过调用LockSupport
的park
方法来执行阻塞当前进程的操作.其实,这里的阻塞就是线程不再执行的含义.通过调用这个函数,线程进入阻塞状态,上述的lock
操作也就阻塞了.等待中断或在独占性变量被释放.
1 |
public static void park(Object blocker) { |
关于中断的相关知识,我们以后再说,就继续沿着AQS
的主线,看一下释放独占性变量的相关操作吧.
ReentrantLock未获得阻塞,加入队列
与lock
操作类似,unlock
操作调用了AQS
的relase
方法,参数和调用acquire
时一样,都是1.
1 |
public final boolean release(int arg) { |
由上述代码可知,release就是先调用tryRelease
来释放独占性变量,如果成果,那么就看一下是否有等待锁的阻塞线程,如果有,就调用unparkSuccessor
来唤醒他们.
1 |
protected final boolean tryRelease(int releases) { |
我们可以看到tryRelease
中的逻辑也体现了可重入锁的概念,只有等到state
的值为1时,才代表锁真正被释放了.所以独占性变量state
的值就代表锁的有无.当state=0
时,表示锁未被占有,否在表示当前锁已经被占有.
1 |
private void unparkSuccessor(Node node) { |
调用了unpark
方法后,进行lock
操作被阻塞的线程就恢复到运行状态,就会再次执行acquireQueued
中的无限for循环中的操作,再次尝试获取锁.
ReentrantLock释放锁并通知阻塞线程恢复执行
有关AQS
和ReentrantLock
的分析就差不多结束了,不得不说,我第一次看到AQS的实现时真是震惊,以前都认为Synchronized
和ReentrantLock
的实现原理是一致的,都是依靠java虚拟机的功能实现的,没有想到还有AQS
这样一个背后大Boss在提供帮助啊.学习了这个类的原理,我们对JUC的很多类的分析就简单了很多,此外,AQS
涉及的CAS
操作和无锁队列的算法也为我们学习其他无锁算法提供了基础.知识的海洋是无限的啊!
转自:http://remcarpediem.com/2017/04/06/AbstractQueuedSynchronizer%E8%B6%85%E8%AF%A6%E7%BB%86%E5%8E%9F%E7%90%86%E8%A7%A3%E6%9E%90/