
在此前的博客什么是AQS中,我们对 AQS 的基本原理进行了简单的介绍,帮助大家对其机制有了一个初步的了解。然而,AQS 作为 Java 并发编程中的核心组件,其设计思想与应用场景远不止于此。为了更深入地剖析 AQS 的强大功能和实际应用,本篇博客将以 **ReentrantLock** 为例,结合实际代码与执行流程,详细解析其背后的实现原理与运行机制。
通过本篇内容,您将了解 **ReentrantLock** 是如何利用 AQS 实现锁的获取与释放、线程的公平性与非公平性,以及如何保障高效并发。我们会从源码的角度入手,逐步揭开 AQS 内部的核心设计,同时结合场景化的示例代码,帮助大家更直观地理解其作用和应用方式。希望通过本篇博客的讲解,能够为大家在深入学习 AQS 和掌握 ReentrantLock 时提供更为实用的参考。
目录
第二种情况:已经有线程获得了AQS的锁,但是获得锁的线程和当前线程是同一个
第三种情况:已经有线程获得了AQS的锁,并且加锁的线程和当前线程不是同一个;
一、AQS简述
AQS全称为AbstractQueuedSynchronizer ,抽象队列同步器,是一个抽象类,主要用来构建锁和同步器。它提供了原子性的管理同步的状态、阻塞线程和唤醒线程的功能。
AQS 为构建锁和同步器提供了一些通用功能的实现,因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如ReentrantLock,Semaphore,CountDownLatch等都是基于 AQS 的。
AQS的两个重要组成:
(1)双向链表队列
由 Node节点组成的双向链表,Node节点保存了等待线程的相关信息。Node节点主要包括三个属性:前序节点pre,线程thread,后序节点next。所以Node是双向的节点。

AQS会将每个获取不到锁的线程(阻塞的线程)封装成node节点放到这个队列里。遵从先进先出原则。
(2)状态信息 state
是一个int类型的成员变量,用于展示当前临界资源的获锁情况。并且使用volatile修饰,来保证变量的可见性。
state 默认为0,表示锁处于未锁定状态。当state=1时,则代表当前对象锁已经被占有,其他线程来加锁时会失败,加锁失败的线程会被放入上面提到的那个双向链表队列中。
state在不同子类中含义不同,ReentrantLock中0表示未加锁,1表示加锁,大于1表示锁重入。
state有三个方法修改,分别是get/set/cas,都是final修饰。
二、ReentrantLock
打开ReentrantLock 的源码你会发现,ReentrantLock 里面有一个抽象类 Sync,ReentrantLock 的所有操作都是基于Sycn对象去操作的。
Sync 继承 AQS,Sync 有两个实现类,分别是FairSync(公平锁)、NonfairySync(非公平锁),而ReentrantLock的加锁和解锁操作就是在这两个类基础上进行的,这也说明了ReentrantLock 里面最终维护的其实就是一个AQS对象。

AQS的初始化
当我们在代码中调用new ReentrantLock()来创建ReentrantLock时,其实就是初始化了一个AQS对象,。默认创建的是NonfairySync,也就是说默认是非公平锁。



初始化的这个AQS对象包括如下几个属性,
- state变量 :锁的状态 0代表未加锁,>0则代表已加锁,和重入次数。
- exclusiveOwnerThread变量:拥有当前锁的线程。
- 由 Node节点组成的双向链表队列,Node节点保存了等待线程的相关信息。
当AQS初始化之后,会初始化一个如下对象,
- state状态为0。
- exclusiveOwnerThread =null。
- 由head 和tail两个空节点组成的首尾双向链表。
执行结果如下图所示,

从例子简单了解
非公平锁下,线程1和线程2的加锁过程
当第一个线程(线程1)开始调用lock.lock()进行加锁的时候,就会开始操作AQS中的部分了。
首先会使用CAS操作compareAndSetState() 原子性,将AQS中的state变量修改为1,因为是第一个线程,所以可以执行成功。成功后,再把AQS中表示获得锁的线程变量exclusiveOwnerThread设置为线程1,表示设置当前获得锁对象的线程为线程1。
这样线程1就执行完了。
接着第二个线程(线程2)来了,也要执行加锁操作。同样也是先执行CAS操作要把AQS中的state变量修改为1,但因为线程1已经把AQS中的state变量修改为1了,表示此时已经有线程获得了AQS的锁,所以线程2修改是失败的。这时,就要把线程2放入队列中了。

但是,此时线程2还没死心还想再尝试一下,这时线程2会再执行CAS操作获取一次AQS中的state变量,判断是否为0,很明显依然不是0。接着,再判断AQS中表示获得锁的线程变量exclusiveOwnerThread是不是线程2自己,表示已经有线程获得了AQS的锁,还没进行释放,此时获得锁的线程和当前线程不是同一个,这步是为了实现可重入锁的功能。
- 如果是同一个线程的话,获取到当前AQS的state变量,执行state+1累计重入次数。
很明显exclusiveOwnerThread变量是线程1不是线程2,又失败了。

到了这里,线程2确实获取不到锁了,真的死心了。
接着就是构建队列中的Node节点了,构建出一个线程2的Node节点,然后执行addWaiter()操作把获得锁不成功的线程加入到阻塞队列。
- 如果当前AQS中双向链表tail节点不为空,则把nodeB设置为tail节点,把nodeB.pre指向原来的tail节点,并把原来的tail节点的nex指向nodeB。
- 如果当前AQS中双向链表tail节点为空,则说明当前链表里面没有其他等待的节点,那么首先创建一个Node 节点(这里定义为nodeN)作为head节点,然后把nodeN.nex指向nodeB节点,把tail指向nodeB节点,nodeB.pre指向nodeN节点。
此处,当前AQS中双向链表tail节点为空,需要再构建出一个没有线程的Node节点(空节点),让AOS中的头节点指向这个新节点,再让尾节点也指向这个新节点。

接着继续执行,把线程2节点挂到头结点的后面,尾节点指向线程2节点。
到这里,AQS中队列的节点就算是创建好了,此时我们就能看到AQS中完整的组成部分了。

接着就是开始操作队列中的节点状态了,这里的操作是在一个循环中。

- 在循环中,首先判断当前节点的前序节点是否为头结点,这么判断是为了保证只有头结点的后序第一个节点才有资格来获取锁。此时,线程2节点的前序节点正好是头结点,头结点的后序第一个节点就是线程2节点,进入条件语句if。
- 线程2要再次获取state变量,判断state变量是否为0,这时候state变量还是1不是0。线程2再一次尝试失败。
- 然后,调用LockSupport.park操作将线程2挂起,到这里线程2算是彻底安静的待在AQS中的队列中了,只能等待其他线程释放锁后来唤醒它。

到这里,线程1和线程2完整的加锁过程就结束了。
非公平锁下,线程1和线程2的解锁过程
接着,咱们看看解锁是怎么样的?
当线程1执行释放锁的时候,会检查AQS中表示获得锁的线程变量exclusiveOwnerThread是不是线程1自己,这里确实是自己。接着会将AQS中的state变量修改为0,接着把AQS中表示获得锁的线程变量exclusiveOwnerThread修改为空。

然后,要判断队列中的头结点必须要存在,在头结点存在的前提下,然后接着要调用LockSupport.unpark操作唤醒头结点的后序第一个节点。到这里,线程1的解锁过程就结束了。

此时,头结点的后序第一个节点就是线程2节点,所以线程2就被唤醒了。因为之前线程2是在一个死循环里被挂起的, 所以被唤醒之后会接着执行循环中的流程。

- 首先,先获取前序节点,判断前序节点是否为头结点,此时确实是头结点。这么判断是为了保证只有头结点的后序第一个节点才有资格来获取锁。
- 判断完后,线程2会获取state变量,判断state变量是否为0,这时候state变量是0了(因为线程1释放锁的时候已经修改了)。既然state变量当前是0,那么线程2就用CAS操作把state变量修改为1。接着把AQS中表示获得锁的线程变量exclusiveOwnerThread修改为线程2。到这里,线程2终于修改成功了。
- 接着,线程2节点把自己修改成头结点,并且把头节点中的线程变量置为空。而且,还要把原来头节点中的后序节点设置为空,这样垃圾回收时就直接能回收掉了。


到这里,线程2就加锁成功了。
公平锁相比于非公平锁有哪里不同呢?
以上说的都是非公平锁的实现。那么这个非公平性体现在哪里呢?让我们回到线程1解锁后,线程2被唤醒的时候。这时,线程2会获取state变量,修改state变量的值。
如果正好有个线程3,也要执行加锁操作, 同样也要获取state变量并修改state变量的值。这就很有可能线程3比线程2先把state变量修改成1了。
明明是线程2先在队列里等着的,结果让后来的线程3给抢先了,这就是非公平锁的体现。

那么,公平锁相比于非公平锁有哪里不同呢?
依然是有个线程3,也要执行加锁操作, 同样也要获取state变量并修改state变量的值。这里额外多了个判断,如果头结点存在着后序节点,而且这个后序节点中的线程不是当前的线程,那么当前的线程就不能去修改state变量的值,只能封装成Node节点放到队列中。

以上就是ReentrantLock的公平锁特点。
AQS的加锁过程
以这段代码为例子,
public class TestAQS {
// 声明一个 ReentrantLock 对象
ReentrantLock reentrantLock = new ReentrantLock();
// 示例方法 demo1
public void demo1() {
reentrantLock.lock(); // 获取锁
try {
// 执行关键代码
demo2(); // 调用另一个方法
} finally {
reentrantLock.unlock(); // 确保锁释放
}
}
// 示例方法 demo2
public void demo2() {
reentrantLock.lock(); // 再次获取锁
try {
// 执行关键代码
System.out.println("Inside demo2");
} finally {
reentrantLock.unlock(); // 确保锁释放
}
}
}
一个线程通过ReentrantLock .lock() 首先会获得当前AQS锁的状态,然后根据锁的对应状态做出不同的处理,具体分为以下几种情况;
- 第一种情况:初次加锁,还没有任何线程获得AQS的锁;
- 第二种情况:已经有线程获得了AQS的锁,但是加锁的线程和当前线程是同一个;
- 第三种情况:已经有线程获得了AQS的锁,并且加锁的线程和当前线程不是同一个;
下面我们针对每种情况进行分析
第一种情况:初次加锁,还没有任何线程获得AQS的锁
线程A调用ReentrantLock .lock() 进行加锁,当还没有任何对象获得AQS锁时候会执行FairSync.tryAcquire()方法,代码如下:

以上标注代码的逻辑是
- 判断队列中是否有正在等待锁的节点,因为是初次加锁,所以这里head和tail节点都是空。
- 使用compareAndSetState()方法对AQS进行加锁,此方法能保证操作的原子性。
- 设置当前获得锁对象的线程。
执行结果如下图所示,

第二种情况:已经有线程获得了AQS的锁,但是获得锁的线程和当前线程是同一个
如上面例子中的代码所示,当线程A调用demo1()方法,已经获得了AQS锁,当调用demo2时又会去竞争AQS锁,这样允许同一个线程多次获得同一把锁的情况称为 "可重入锁"
当线程A再次调用ReentrantLock .lock() 时,会执行FairSync.tryAcquire()对应重入锁逻辑,代码如下,

以上标注代码的逻辑:
- 首先拿当前线程和已经获得AQS锁的线程对比是否是同一个线程。
- 是同一个线程的话获得当前AQS的state变量,执行state+1累计重入次数。
- 修改AQS的state变量。
执行结果如下图所示,

第三种情况:已经有线程获得了AQS的锁,并且加锁的线程和当前线程不是同一个;
当前面的线程A已经获得了AQS锁,还没进行释放,此时线程B调用ReentrantLock .lock() 方法获取锁会执行AbstractQueuedSynchronizer.acquire(),代码如下:


以上代码逻辑:
1)首先执行 addWaiter()把获得锁不成功的线程加入到阻塞队列,
- 把线程B封装为一个Node节点nodeB。
- 如果当前AQS中双向链表tail节点不为空,则把nodeB设置为tail节点,把nodeB.pre指向原来的tail节点,并把原来的tail节点的nex指向nodeB。
- 如果当前AQS中双向链表tail节点为空,则说明当前链表里面没有其他等待的节点,那么首先创建一个Node 节点(这里定义为nodeN)作为head节点,然后把nodeN.nex指向nodeB节点,把tail指向nodeB节点,nodeB.pre指向nodeN节点。
2)addWwaiter()成功后调用 acquireQueued()方法
- 首先会再次尝试获取一下锁。
- 当获取锁失败后,把双向链表中node2B.pre指向的节点的waitStatus 设置为 -1。
- waitStatus=0 新加的节点,处于阻塞状态。
- waitStatus= 1 表示该线程节点已释放(超时、中断),已取消的节点不会再阻塞。
- waitStatus=-1 表示该线程的后续线程需要阻塞,即只要前置节点释放锁,就会通知标识为 SIGNAL 状态的后续节点的线程 。
- waitStatus=-2 表示该节点的线程处于等待Condition条件状态,不会被当作是同步队列上的节点,直到被唤醒(signal),设置其值为0,重新进入阻塞状。
- waitStatus=-3 表示该线程以及后续线程进行无条件传播(CountDownLatch中有使用)共享模式下, PROPAGATE 状态的线程处于可运行状态。
执行结果如下图所示,

最后整个AQS初始化、线程A两次加锁、线程B加锁的流程,如下图所示:

AQS的解锁过程
通过上面的流程,我们已经了解了AQS的加锁过程,当线程加锁不成功之后,会把当前线程放到一个等待队列中去,这个队列是由head和tail构建出来的一个双向链表,下面我们继续上面的案例继续分析AQS的解锁过程。
因为上面AQS的锁获得线程为线程A ,所以现在只有线程A 可以进行释放锁,当线程A 调用ReentrantLock .unlock() 时,最终执行ReentrantLock.tryRelease()方法,代码如下:
- 获得当前AQS的state,并进行减1(state每减1代表释放一次锁);
- 当state=0的时候说明当前锁已经完全释放了,此时会设置拥有AQS 锁的线程为null;
- 当state不等于0说明锁还没有释放完全,此时修改state的值。

执行结果如下图所示,

当线程A释放完锁之后,程序会调用AbstractQueuedSynchronizer.release()方法的,代码如下,

如果明白了AQS的加锁过程,那么你已经猜到了,当线程释放锁完毕之后接下来肯定是唤醒等待队列里面的线程了,这段代码也的确是在做这些事情:
- 获得等待队列链表中的head节点。
- 当head节点不为空,并且head节点的waitStatus!=0(这里代表线程状态正常)时,调用unparkSuccessor()方法唤醒链表中head.next节点中的线程。
- 当前链表里面head.next 为nodeB,所以线程B会被唤醒,然 后重新去获取锁,同时重构链表节点。
执行结果如下图所示,

最后,线程B释放锁的流程也是如此。执行结果如下图所示,

公平锁非公平锁的区别
公平锁进行lock()的时候,如果AQS为无锁状态,公平锁首先会判断AQS里面是否有等待的线程,如果有的话会添加到队列里面排队,队列里面没有线程的话才会去尝试获取锁。
非公平锁进行lock()的时候,只要是AQS是无锁状态,不管队列里面是否有等待线程都会直接去尝试获得锁。
2万+

被折叠的 条评论
为什么被折叠?



