AQS、ReentrantLock详解
深入理解Java并发框架AQS系列(一):线程 - 昔久 - 博客园;
⛵ReentrantLock
🚍简介
ReentrantLock是一个可重入且独占式的锁,基于AQS实现【AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架从ReentrantLock的实现看AQS的原理及应用 | JavaGuide】,ReentrantLock和 synchronized 关键字类似。不过,ReentrantLock 更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。
🛥️设计思想
作为一把锁,其最有个重要的功能就是将没有获取到锁的线程进行阻塞,然后等到有锁的时候再将线程继续运行 ,如果需要将线程进行阻塞,我们可以采用wait()、sleep()、park()、循环的方式进行线程阻塞。还需要定义一个锁的状态,表示锁已经被占有,并且设置锁的状态我们需要原子操作。基于以上设计思想我们可以自己实现一个简单的ReentrantLock。
- 首先我们要
csharp
/**
* 自定义锁
* @author hanLin.liu
* @create 2024-11-12 10:19
*/
public class MyReentrantLock {
/** 锁的占有状态 0-未被占有 大于等于1表示占有 */
private volatile int status;
/** 表示持有锁的当前线程 */
private volatile Thread currentThread;
/** 用来处理原子操作 */
private static Unsafe unsafe;
/**
* 解锁方法
*/
public void unlock(){
unsafe.getAndAddInt(this, getStatusOffset(), -1);
}
/**
* 加锁方法
*/
public void lock(){
// 加锁成功跳出循环,加锁失败一直循环阻塞
while (!setStatus()){}
}
/**
* 进行加锁
* @return
*/
private boolean setStatus(){
// 当锁状态为0,可以加锁
if (status == 0){
boolean b = unsafe.compareAndSwapInt(this, getStatusOffset(), 0, 1);
if(b){
currentThread = Thread.currentThread();
return true;
}
}else if(currentThread == Thread.currentThread()){
unsafe.getAndAddInt(this, getStatusOffset(), 1);
return true;
}
return false;
}
/**
* 获取status在对象中的偏移量
* @return
*/
private long getStatusOffset(){
try {
return unsafe.objectFieldOffset(MyReentrantLock.class.getDeclaredField("status"));
} catch (NoSuchFieldException e) {
e.printStackTrace();
return -1;
}
}
/**
* 获取 Unsafe 实例
*/
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
定义完自定义锁后,我们来个方法测试一下
csharp
public static void main(String[] args) {
MyReentrantLock lock = new MyReentrantLock();
for(int i = 1; i <= 10 ; i++){
new Thread( () -> {
System.out.println("【" + Thread.currentThread().getName() + "】:开始抢锁...");
lock.lock();
System.out.println("【" + Thread.currentThread().getName() + "】:抢锁成功...");
System.out.println("【" + Thread.currentThread().getName() + "】:执行临界区代码...");
System.out.println("【" + Thread.currentThread().getName() + "】:释放锁...");
lock.unlock();
}, "线程" + i).start();
}
}
执行后输出如下,可以看出我们自定义的这个锁可以实现一个简单的锁功能。

上述代码我们实现了一个简单的自定义lock,但是还存在许多的问题,比如线程大量自旋,线程饥饿等 ,对于这些问题,我们可以使用AQS进行解决,我们现在来看一下ReentrantLock的源码以及实现原理。
✈️AQS
⛰️简介
AQS是一个抽象类,其内部定义了同步队列、Condition单向链表等数据结构,对外提供了一系列的方法,用于构建定制化的锁和同步器;然后AQS内部也封装了一系列的原子操作,使用AQS可以非常轻松的实现一个自定义的锁。
🗻 CLH
AQS的设计是借鉴了CLH锁的,CLH是一种自旋锁,可以实现线程之间排队等待锁,避免线程饥饿。我们来看一下代码。
java
/**
* CLH锁
* @author hanLin.liu
* @create 2024-11-26 14:01
*/
public class CLHLock {
// 用来表示当前线程节点
ThreadLocal<Node> cruNode = ThreadLocal.withInitial(Node::new);
// 用来表示尾指针
AtomicReference<Node> tail = new AtomicReference<>();
class Node{
volatile boolean locked = false;
}
CLHLock(){
tail.set(new Node());
}
public void lock(){
final Node cru = cruNode.get();
// 表示加锁
cru.locked = true;
// 表示得到前驱节点,将尾指针设置成当前节点
Node pre = tail.getAndSet(cru);
// 如果前驱节点没有释放锁就一直自旋等待,等到
while (pre.locked);
}
public void unLock(){
// 相当于将当前线程解锁
final Node node = cruNode.get();
node.locked = false;
// 这里如果不写这一行代码的话,可能会导致死锁
// 假设现在释放锁之后,在后面的线程还没有抢到锁之前,这个线程又加锁了,对应的节点还是之前那个,这个线程就会一直自旋等待上一个线程释放锁
cruNode.set(new Node());
}
}
根据上述代码我们来画一个图
这是每个线程对应Node节点的结构图,可以看出通过ThreadLocal,每个线程相当于都有一个locked字段,来表示后续线程是否要进行自旋。

当第一个线程执行lock()方法时,tail设置指向T1(线程一)对应的Node节点并且返回初始化节点,由于初始化节点的locked为false,不需要自旋,所以T1加锁成功。如下图

当第二个线程T2再执行lock()方法时,tail设置指向T2对应的Node2节点并且返回T1对应的Node1,由于Node1的locked为true,所以T2会进行自旋,所以T2阻塞直到T1释放锁。如下图

当T1调用unlock()方法释放锁时,Node1的locked被设置成false,T2检查到Node1的locked为false,停止自旋,加锁成功,如下图

通过上述代码和图可以了解到CLH的一个结构以及加锁解锁,自旋的过程,
CLH存在的问题?
- 由于没有获取到锁的线程都会进行自旋,如果抢锁的线程比较多的情况下,会导致系统大量线程自旋,导致性能下降,这点在AQS中进行了改进,AQS只让队列中第一个等待的节点自旋,对其他线程进行
park - 基本的 CLH 锁功能单一,不改造不能支持复杂的功能。
🚁队列Node节点
我们先来看一下Node节点的源码
arduino
// Node 是 AQS 内部实现的一个核心数据结构
static final class Node {
// 表示共享锁的节点
static final Node SHARED = new Node();
// 表示独占锁的节点
static final Node EXCLUSIVE = null;
// 状态:线程状态
static final int CONDITION = -2; // 线程处于等待条件的状态
static final int CANCELLED = 1; // 线程被取消
static final int SIGNAL = -1; // 线程被设置为可被唤醒
static final int RUNNING = 0; // 线程处于运行状态
volatile int waitStatus; // 线程的等待状态
volatile Node prev; // 前驱节点
volatile Node next; // 后继节点
volatile Thread thread; // 当前节点所对应的线程
Node nextWaiter; // 下一个等待者(仅用于条件变量的队列)
// 构造函数
Node(Thread thread) {
this.thread = thread;
}
// 构造函数,带状态
Node(Thread thread, int waitStatus) {
this.thread = thread;
this.waitStatus = waitStatus;
}
// 判断节点是否处于共享模式
final boolean isShared() {
return nextWaiter == null;
}
// 设置节点状态
static Node newNode(Thread thread, int waitStatus) {
return new Node(thread, waitStatus);
}
static Node newConditionNode() {
return new Node(null, CONDITION);
}
}
我们主要解释一下作用waitStatus的作用
-
waitStatus:
CANCELLED(1):表示线程因为超时或者中断而被取消。节点一旦被设置为CANCELLED状态,它将不会被再次使用。SIGNAL(-1):表示节点的后继节点正在(或即将)被阻塞(通过park操作),因此当前节点在释放或取消时需要唤醒它的后继节点。CONDITION(-2):表示节点当前在条件队列中。它将不会用于同步队列,直到被转移到同步队列中(当条件被满足时)。PROPAGATE(-3):共享模式下,头节点可能会被设置为PROPAGATE状态,以确保唤醒后继节点。0:表示节点在初始状态,新创建的节点默认是这个状态。
根据此源码,我们可以画出一个节点的结构图

通过该Node的数据结构,可以组成一个队列,用于维护阻塞和等待的线程,能按照正确的顺序被唤醒和执行。
🛫Condition单向链表
Condition链表是用来存放调用了await()方法的线程节点,调用signal()方法用来将在Condition链表中的节点加入到同步队列中进行排队抢锁。使用Node节点构成Condition链表时,结构图如下

🚀同步队列
AQS中有个head和tail,用于当线程调用tryAcquire()方法获取尝试获取锁失败时,会调用addWait()方法将其线程创建一个对应的Node节点,并且将该节点加入到队列中。

🌌源码分析
我们这里以ReentrantLock举例来分析AQS的源码,我们查看ReentrantLock源码,我们可以看到有两个构造器,一个默认创建的是非公平锁,一个带参的可以控制创建非公平锁还是公平锁。

我们查看其lock()方法,可以发现其调用的sync.lock()方法,sync.lock()有两种方式,公平锁方式和非公平锁方式,如图

我们以公平锁来进行举例分析。我们点进去后看到acquire(int arg)方法,这是AQS类的方法这里我们解释一下该方法

scss
public final void acquire(int arg) {
// 如果加锁失败&&线程停止=>设置线程状态为中断
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
java
abstract static class Sync extends AbstractQueuedSynchronizer {...}
// 公平锁的实例
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
// 调用公平锁的加锁方法
final void lock() {
// 【1】调用AQS的获取锁
acquire(1);
}
// 【3】尝试获取锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 如果锁状态为未被占有
if (c == 0) {
// 如果加锁的这个线程不处于队列的第一个&&设置锁状态成功&&设置持有锁的线程成功=>加锁成功
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果锁状态已经被占有,判断占有锁的线程是否为当前线程,是当前线程则将锁状态+1表示可重入锁=>加锁成功
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 否则加锁失败
return false;
}
}
// AQS类
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
// 锁状态
private volatile int state;
// 指示节点正在以独占模式等待的标记
static final Node EXCLUSIVE = null;
// 【2】获取锁,入参为1
public final void acquire(int arg) {
// 加锁失败&&该节点不是第一个排队的节点&&
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 当前线程是否处于队列的第一个排队节点中,也就是说需不需要进行排队
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
// 【4】为当前线程和给定模式创建节点并将其排入队列
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
// 如果尾节点不为null,也就是说该节点不是第一个入到等待队列里的节点
if (pred != null) {
node.prev = pred;
// 原子操作将tail节点设置为当前节点,如果成功则将前置节点的next指向当前节点并返回,
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果尾节点为null,则说明是第一个入等待队列的节点
enq(node);
return node;
}
// 【5】如果是第一个入队列的节点或者第一次加入队列失败,需进行CAS自旋加入等待队列中
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
// 【6】这个方法就是判断是否为第一个排队的节点,如果是第一个排队的,则自旋,不是则进行park,在这里进行park后
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.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
}