前言
上篇博客讲解了Sychronized的底层实现原理,它是基于jdk实现的,现在此篇博客讲解基于AQS 框架实现的另一个锁ReentrantLock
1、名词解释
为什么叫CLH队列
CLH 全称是三个人创造的,分别为Craig、Landin、Hagersten,所以以这个三个名字的首字母命名,原始的CLH锁是一种单向链表结构的自旋锁队列
每个节点仅包含:
前驱指针(myPred):指向队列中前一个节点
状态变量(locked):表示当前线程是否需要获取锁(true=需要自旋等待,false=可获取锁)
线程信息:封装当前等待线程
但是在ReentrantLock里它对原来的CLH队列进行了升级,Java 并发包中的 AQS 框架采用 CLH 锁的变体(虚拟双向队列(FIFO队列)),通过封装线程为 Node 对象实现锁分配。
2、ReentrantLock的实现
2.1 Node的数据结构
在AQS框架中,是使用队列的方式来实现同步管理的,队列中每个节点是Node节点,看下Node的数据结构,
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
源码如上,其实核心的结构就是
thread:一个等待获取同步状态的线程
prev:指向上一个节点的引用
next:指向下一个节点的引用
waitStatus:等待的状态:
1.CANCELLED:值尾1,表示在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消等待,接入CANCELLED状态后将不会再变化
2.SIGNAL:值尾-1,表示后续节点的线程处于等待状态,如果当前节点的线程释放了同步状态或者被取消便会通知后续节点,使后续节点的线程得以运行
3.CONDITION,值为-2 表示节点再条件队列中,节点等待线程再Condition上,当其它线程队Condition调用了signal()后,该节点将会从条件队列中转移到同步队列中
4.PROPAGATE:值为-3,表示下一次共享式同步状态获取将会无条件传播下去nextWaiter:表示条件队列中的后续节点,如果当前节点是共享的,那么这个字段将是一个SHARED变量static final Node SHARED = new Node();,也就是说节点类型(独占和共享)和条件队列中的后续节点共用同一个字段EXCLUSIVE:独占模式节点
AQS实现原理
上边介绍了AQS 中的Node节点,现在说下AQS实现的原理
核心元素
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
// Node节点
static final class Node {...}
// 指向头节点
private transient volatile Node head;
// 指向尾节点
private transient volatile Node tail;
// 表示资源的可用状态
private volatile int state;
}
AQS 中的同步等待队列结构
AQS中 维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。
这里volatile能够保证多线程下的可见性,当state=1则代表当前对象锁已经被占有,其他线程来加锁时则会失败,加锁失败的线程会被放入一个FIFO的等待队列中,比列会被LockSupport.park()操作挂起,等待其他获取锁的线程释放锁才能够被唤醒。另外state的操作都是通过CAS来保证其并发修改的安全性。锁的可重入性state也会有所体现,当state等于0说明没有其它线程获取锁,可以被获取,获取后会设为1并将exclusiveOwnerThread设置为获取到锁的线程,如果不是等于0则判断是否是exclusiveOwnerThread的线程,是的话则获取锁,state+1,否则进入等待队列进行排队。需要注意的是head 指向的是一个虚拟节点,该节点不关联任何线程(thread=null),仅作为队列的起始标识。这种设计简化了队列操作,避免在无竞争时频繁创建和销毁节点
源码分析
1、ReentrantLock下的lock方法
public ReentrantLock() {
sync = new NonfairSync();
}
public void lock() {
sync.acquire(1);
}
获取锁
//尝试获取锁,如果成功则直接返回,如果失败则调用 addWaiter(Node.EXCLUSIVE) 将当前线程加入等待队列,并调用 acquireQueued 进行排队等待。
//如果在排队过程中线程被中断,则调用 selfInterrupt() 恢复中断状态。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 非公平锁
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
// 同步状态c
int c = getState();
//如果状态值 c 为 0(表示锁未被占用),尝试通过 CAS 操作将状态设置为 acquires,成功则设置当前线程为独占线程并返回 true
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果状态值 c 不为 0 且当前线程已经是独占线程(自己),则更新状态值为 c + acquires,如果发生溢出则抛出异常,否则返回 true。
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;
}
final boolean acquireQueued(final Node node, int arg) {
// 初始化中断标志为 false。
boolean interrupted = false;
try {
for (;;) {
//进入无限循环,获取当前节点的前驱节点 p:
final Node p = node.predecessor();
//如果前驱节点是头节点且成功获取锁(tryAcquire(arg) 返回 true),则将当前节点设置为头节点,
//并清理前驱节点的引用以帮助垃圾回收,返回中断标志
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
if (interrupted)
selfInterrupt();
throw t;
}
}
//添加等待队列节点
private Node addWaiter(Node mode) {
// 创建一个新节点 node,其模式由参数 mode 指定 这里mode=EXCLUSIVE 独占式
Node node = new Node(mode);
for (;;) {
Node oldTail = tail;
//如果队列尾部 tail 不为空,设置新节点的前驱为当前尾节点,并通过 CAS 操作更新尾节点。
if (oldTail != null) {
node.setPrevRelaxed(oldTail);
//如果 CAS 成功,将原尾节点的 next 指向新节点并返回新节点
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
return node;
}
} else {
//如果 tail 为空,初始化同步队列
initializeSyncQueue();
}
}
}
//使用 HEAD.compareAndSet 方法尝试将头节点从 null 设置为新创建的节点 h,
//如果设置成功,则将尾节点 tail 指向该新节点 h。
private final void initializeSyncQueue() {
Node h;
if (HEAD.compareAndSet(this, null, (h = new Node())))
tail = h;
}
// 释放锁
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
跟据源代码生成的流程图
2 head节点的核心作用
(1)虚拟节点的标识与初始化
虚拟节点设计:head通常指向一个 虚拟节点(dummy node) ,该节点不关联任何线程(thread=null),仅作为队列的起始标识。这种设计简化了队列操作,避免在无竞争时频繁创建和销毁节点。
延迟初始化:队列初始化时,head和tail均为null。当第一个线程竞争资源失败时,AQS通过enq()方法创建一个虚拟节点,并设置head和tail指向它,随后将实际线程节点追加到尾部。
(2)锁持有者的标识
资源持有者标识:当线程成功获取锁(如通过tryAcquire),它会将自身节点设为新的head,并清空其thread和prev字段。此时,原head节点被移出队列,帮助GC回收资源。
唤醒逻辑的起点:释放锁时,AQS从head.next开始唤醒后续节点,确保FIFO顺序。若head.next为空(如节点被取消),则从尾部向前遍历找到有效节点。
(3)线程安全与状态管理
CAS原子操作:head的更新通过compareAndSetHead()方法保证原子性,防止多线程竞争导致队列结构破坏。
状态传递:head节点的waitStatus(如SIGNAL)用于协调后继节点的阻塞与唤醒。例如,当节点状态为SIGNAL时,表示其后继节点需要被唤醒。
3、非公平锁和公平锁的实现方式
其实非公平锁和公平锁的有两处不同
非公平锁
*/
@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
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;
}
公平锁
@ReservedStackAccess
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;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。
相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。