【Java 并发】【九】【AQS】【四】ReentrantLock是怎么基于AQS实现独占锁的
1 前言
先回顾下我们前面讲解的,AQS作为基础的并发框架为我们提供了:
(1)AQS作为一个并发的基础框架,定义了资源,规定了获取资源的入口、释放资源的入口,定义了等待队列,同时还有一套机制规定了获取锁失败的线程进入等待队列等待,以及资源释放的时候怎么唤醒等待队列的线程重新竞争锁等。
(2)深入的剖析了AQS独占锁的源码和核心的流程机制,剖析了获取锁失败进入等待队列的源码流程,以及进入等待队列是否要进行线程挂起,什么时候再去重新竞争锁;资源释放的时候唤醒等待的线程的流程源码
(3)深入剖析了AQS提供共享锁机制,这里与进入等待队列和在等待队列中重新竞争锁的流程和独占锁几乎一致。但是不同的点在于共享锁资源充足的时候是有传播机制的,会不断唤醒等待队列中沉睡的线程,直到资源传播完毕为止。
接下来就讲解的ReentantLock是基于之前讲解的AQS来实现的,它是建立在AQS体系之上的一个并发工具类。
2 ReentantLock是什么?
ReentrantLock主要是以下四个点:
(1)ReentrantLock是一个基于AQS来实现的可重入的互斥锁,它是一个互斥锁,并且支持可重入的。
(2)ReentrantLock实现了Lock接口,实现了锁的语义。
(3)ReentrantLock提供了两种锁模式,公平锁和非公平锁,默认是非公平锁。
(4)ReentrantLock基于AQS之上的Condition机制,实现了多线程通过Condition进行睡眠、唤醒来控制线程行为
我们写个例子来感受下:
public class ReentrantLockTest extends Thread{ // 共享变量 private static int num = 0; // 互斥锁 这个例子如果去掉static会发生什么呢? 这把锁就不是同一把了,就会失效导致结果不对 private static ReentrantLock lock = new ReentrantLock(); // 累加 private void add() { try { // 先获取锁 lock.lock(); num++; } finally { // 最后释放锁 lock.unlock(); } } @Override public void run() { for (int i=0; i<10000; i++) { add(); } } public static void main(String[] args) throws InterruptedException { // 创建两个线程并启动 ReentrantLockTest demo1 = new ReentrantLockTest(); ReentrantLockTest demo2 = new ReentrantLockTest(); demo1.start(); demo2.start(); // 等待两个线程都执行完 demo1.join(); demo2.join(); // 打印一下结果 System.out.println(ReentrantLockTest.num); } }
我们接下来看一下ReentrantLock里面的锁机制是怎么实现的。
3 ReentrantLock锁机制原理
说起ReentrantLock,它内部有一个Sync的内部类,这个Sync继承了AQS,同时Sync又有两个子类,分别是NonfairSync非公平锁、FairSync公平锁。然后ReentrantLock只是基于已有的公平锁和非公平所同步工具类之上再简单的封装了一层而已。大概的结构图是这样的:
之前讲AQS的时候啊,说过AQS提供了四个空的方法,这四个方法分别是:
(1)分别是获取独占锁实现方法tryAcquire、释放独占锁实现方法tryRelease
(2)获取共享锁实现方法tryAcquireShared、释放共享锁实现方法tryReleaseShared
AQS的子类分别实现这四个不同的方法,就形成了不同的并发工具。按照AQS定义的规范,这里ReentrantLock来说,具有独占锁的功能,其实就必须要实现AQS的tryAcquire、tryRelease方法。公平锁FairSync和非公平锁NonfairSync都是实现了tryAcquire和tryRelease方法。
3.1 公平锁内部原理
我们首先来看下FairSync公平锁的内部源码:
3.1 公平锁FairSync获取锁逻辑
公平锁同步器重写了AQS的tryAcquire方法:
static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { // 这里的acquire方法其实就是AQS的acquire方法 acquire(1); } protected final boolean tryAcquire(int acquires) { // 获取当前线程 final Thread current = Thread.currentThread(); // 判断资源是否有人加锁,c == 0 没人加锁,c > 0 表示有人加锁了 int c = getState(); // c == 0 表示没人加锁 if (c == 0) { // hasQueuedPrecessors这里时判断AQS的等待队列是否有人在等待 // 其实公平锁和非公平锁实现的精髓就在这里, // 公平锁如果发现AQS中等待队列有人在等待,那么直接去排队,即时资源时空的也不争抢 if (!hasQueuedPredecessors() && // 如果AQS队列没线程在排队,则CAS开始争抢锁 compareAndSetState(0, acquires)) { // 争抢成功则设置加锁的线程是自己 setExclusiveOwnerThread(current); return true; } } // 如果上面 c > 0 说明有人加锁了 // 这里就获取当前加锁的线程是谁,如果加锁的是自己,则直接重入 else if (current == getExclusiveOwnerThread()) { // 之前加锁的是自己,现在直接重入,修改加锁的次数就好了 int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } }
我们这里梳理一下FairSync公平锁的acquire方法的源码流程如下:
(1)调用ReentrantLock的公平锁的lock方法其实会调用到AQS的acquire方法。
acquire的方法内部其实调用子类的tryAcquire方法去实际获取锁,也就是回到了子类FairSync的tryAcquire方法,源码的流程如上图
(2)这里tryAcquire的方法其实做的事情很简单,如果判断c == 0 资源是空的,看一下等待队列有没有人,有人直接去队列后面等待,这就是公平锁!(公平锁就是判断等待队列有人就去乖乖排队了),如果没人等待自己就去加锁。
如果资源 c > 0,说明已经有人加锁了, 则判断之前加锁的人是不是自己,如果是自己说明自己已经加过锁了,直接修改加锁的次数就好了,如果不是自己说明别人加锁了,自己自然就获取锁失败了
(3)一旦在tryAcquire获取锁失败,就会调用AQS的addWaiter方法进入等待队列排队,然后调用acquireQueued自旋的尝试获取锁或者将自己挂起,等待别人唤醒。这里的源码和核心机制,前几节我们已经分析过了。
其实也就是实现了获取锁的tryAcquire方法逻辑,子类实现获取独占锁,只需要实现tryAcquire方法就可以了,底层的进入等待队列addWaiter、以及阻塞等待的acquireQueued的这套机制还是AQS的。
3.2 公平锁FairSync获取锁逻辑
我们看到ReentrantLock释放锁的方法为unlock,底层调用的还是Sync的release方法,源码如下:
public void unlock() { sync.release(1); }
这里的release方法,其实就是AQS内部提供的release方法,作为释放独占锁资源的入口,我们之前讲过源码了,如下:
public final boolean release(int arg) { // 1. 这里会调用子类的tryRelease方法,实际去释放锁 if (tryRelease(arg)) { Node h = head; // 2. 释放锁成功,唤醒后续AQS等待队列中等待的线程 if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
上面的源码我们知道,释放锁的实际逻辑还是到子类的tryRelease方法里面,也就是FairSync的tryRelease方法里面。由于FairSync继承自Sync同步器,同时也继承了Sync中的tryRelease方法,Sync的tryRelease释放锁的实际逻辑,源码如下:
protected final boolean tryRelease(int releases) { // 直接将释放锁的次数减少releases次 // 也就是你加锁多少次,就释放多少 // 当state == 0 的时候说明锁已经空闲了,没人持有了 int c = getState() - releases; // 这里释放之前判断之前是不是自己加锁的 // 如果自己之前没加锁,不能胡乱释放,直接抛出异常 // 谁加的锁,谁才能释放 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; // 当c == 0 说明锁已经完全释放了 if (c == 0) { free = true; // 设置加锁的线程为null,表示没人加锁了 setExclusiveOwnerThread(null); } // 设置state = 0,锁空闲了,让别的线程可以加锁 setState(c); // free = true表示锁释放了,完全空闲了 return free; }
我们同样画个图来梳理一下释放锁的流程图:
(1)释放锁的时候其实首先会走入到AQS提供的release入口方法,然后release中再调用子类的tryRelease方法去释放锁
(2)tryRelease释放锁的时候会判断是不是加锁的线程释放的,如果不是,会抛异常,只能加锁的人释放锁,不然就乱了。
然后释放锁的时候会扣减加锁的次数,当state 加锁的次数被扣减为零,才是真正的完全释放锁,这个时候就需要设置一下加锁的线程为null,也就是没人加锁了。
(3)当锁释放了之后,就会调用AQS给你提供的unparkSuccessor方法去唤醒等待队列中正在等待的人了,这里的源码之前我们非常详细的讲解过了
基于AQS,只需要实现获取锁和释放锁的逻辑就好了,至于获取锁失败之后要干啥,AQS已经给你定义好了,那就是进入等待队列,然后你可能在队列里面沉睡挂起,或者自旋再次尝试获取锁。同样的当你释放锁成功之后要干啥,AQS同样已经定义好了,那就是唤醒等待队列中的线程让他们去重新竞争锁;这些都是AQS内部已有的机制。
4 非公平锁内部原理
接下来讨论ReentrantLock里面的另外一个锁,非公平锁NonFairSync。
4.1 非公平锁NonFairSync获取锁逻辑
上面公平锁FairSync的加锁实现,当state == 0的时候也就是没人加锁的时候,它会调用AQS的hasQueuedPredecessors()方法,判断等待队列里面是否还有人在等待。如果有人在等待那它就不去加锁了,如果没人在等它就尝试去加锁。那么接下来我们看一下非公平锁怎么实现的。
首先看一下NonFairSync非公平锁的加锁方法lock方法,作为加锁的入口方法:
final void lock() { // 这里上来就直接尝试加锁,不管资源是不是空的,不管有没有人在等待 // 非公平锁看到,上来就抢 if (compareAndSetState(0, 1)) // 抢夺成功之后,设置是自己加锁,然后就完事了 setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
看上面非公平锁的加锁lock方法:
(1)首先进入的这个方法,立马执行compareAndSetState(0,1) 尝试去加锁,这个时候不管有没有人加锁,也不管等待队列中有没有人在等,完全不讲规矩
(2)然后尝试加锁失败才调用AQS的acquire方法,我们继续看流程:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
(3)然后acquire方法里面又会调用子类的tryAcquire方法,也就是调用NonfairSync的tryAcquire方法,实际尝试获取锁:
protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); }
(4)NonFairSync的tryAcquire方法直接内部就是调用到Sync的nonFairTryAcquire方法,也就是说调来调去,加锁的具体逻辑最核心的源码在nonfairTryAcquire方法内部,我们下面看看:
final boolean nonfairTryAcquire(int acquires) { // 获取当前线程 final Thread current = Thread.currentThread(); // 获取资源state的值,state > 0 表示已经有人加锁了 // state = 0表示没人加锁,锁是空闲的 int c = getState(); // 如果c == 0 没人加锁,马上就去竞争锁,不管有没有人在等待 if (c == 0) { // CAS尝试竞争锁 if (compareAndSetState(0, acquires)) { // 加锁成功,设置加锁的线程是自己, // 这里的setExclusiveOwnerThread就是设置是哪个线程加锁的 setExclusiveOwnerThread(current); return true; } } // 如果上面c != 0 ,说明有人加锁了;这里判断之前加锁的线程是不是自己 // 如果是自己的话,直接就重入,直接把自己加锁的次数增加就可以了 // 如果不是自己加锁,说明是别人加锁了,此时就需要进入AQS的等待队列等待 else if (current == getExclusiveOwnerThread()) { // 加锁的时自己,直接增加自己加锁的次数就可以 int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
这里非公平锁NonFairSync的源码跟之前公平锁的tryAcquire方法源码几乎一致,不同的是:
(1)公平锁:唯一不同的是公平锁在资源state == 0也就是没人加锁的时候,通过hasQueuedPrecessors()方法判断等待队列有没有在等待,如果有人在等待则它立马放弃去加锁。
(2)非公平锁:非公平锁在state == 0 也就是没人加锁的时候,才不管你等待队列有没有人在等待,它不在乎,比较自私一点,直接就去争抢锁,成功就返回了。
4.2 非公平锁NonFairSync释放锁逻辑
对于释放资源的实际方法tryRelease,公平锁和非公平所完全一样,都是使用Sync的tryRelease方法。
public void unlock() { // 调用到AQS的release方法释放 sync.release(1); }
然后AQS中的release方法调用到子类Sync的tryRelease方法:
public final boolean release(int arg) { // 调用到子类的tryRelease方法 if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
上面已经详细讲解了tryRelease方法释放资源的流程了。
5 小结
本节我们讲解了ReentrantLock内部有公平锁FairSync、NonFairSync非公平锁两种锁同时对这两种锁的实现和源码分析,这两种锁的区别就在于获取锁的时候判断等待队列有没有人,公平锁在等待队列有人的时候去等待;非公平锁则不管,直接去争抢,下节我们讲解ReentrantLock的Condition机制,这个机制能实现类似wait和notify类似的控制线程沉睡、唤醒的行为功能,有理解不对的地方欢迎指正哈。