AQS源码解析——ReentrantLock独占锁篇
ReetrantLock
是Java juc包的一部分,它通过AQS来控制多个线程对关键代码的访问。通常被用作synchronized
关键字的替代,相比synchronized
,它更加灵活可控。
快速上手
1. 创建实例
import java.util.concurrent.locks.ReentrantLock;
ReentrantLock lock = new ReentrantLock();
2. 获取和释放锁
lock.lock();
try {
// 关键代码段
} finally {
lock.unlock(); // 总是在 finally 块中释放锁,以确保即使抛出异常也能释放锁。
}
ReentrantLock 锁的特性
- 可重入:线程可以多次获取相同的锁。加多少次锁就要释放多少次锁,以便其他线程可以获取。
- 可中断:
ReentrantLock
支持可中断锁定,就是说等待锁的线程可以被另外一个线程中断,而等待线程可以友好的处理中断。 - 锁条件:
ReentrantLock
锁支持条件变量,简单说就是锁在争抢资源或阻塞等待前先去执行特殊条件newCondition()
,此篇不会涉及,关注BlockingQueue
篇。 - 公平与否:R锁支持公平锁和非公平锁,言简意赅来说公平锁就是先来后到的排队策略,通过阻塞队列的顺序来依次尝试获取锁资源;而非公平锁是总有队外线程和阻塞在队列里的线程同一时刻去争抢锁资源。
源码分析
1.构造函数和基本属性
ReentrantLock
的构造函数可以接受一个 boolean 值参数,用于指定是否使用公平策略。如果传入 true
,则会启用公平策略,即等待时间最长的线程会优先获取锁。
ReentrantLock reentrantLock = new ReentrantLock(true);
public ReentrantLock(boolean fair) {
//FairSync实现公平锁、NonfairSync非公平,默认为false
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock
内部有个成员类Sync
继承AQS来实现同步功能,FairSync
和 NonfairSync
是进一步的实现
static final class FairSync extends Sync {//此处省略}
static final class NonfairSync extends Sync {//此处省略}
abstract static class Sync extends AbstractQueuedSynchronizer {}
2.加锁
reentrantLock.lock();
lock方法通过调用Sync lock方法来实现
public void lock() {
sync.lock();
}
非公平锁的加锁逻辑
static final class NonfairSync extends Sync {
final void lock() {
//此处修改的state是AQS中定义的成员变量,标识同步器的状态信息。state在不同的同步实现中有不同含义
//在此处state用来表示锁的状态,如果state为0,表示锁未占用的状态,大于0表示占用状态
//此处要获取锁,所以通过CAS将state设置为1,本系列篇章不对CAS做过多说明
if (compareAndSetState(0, 1))
//将当前运行线程设置为独占线程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
AQS 类提供的方法,用来实现同步器获取锁的操作
public final void acquire(int arg) {
if (!tryAcquire(arg) //尝试获锁,AQS无实现,需要子类来实现,此处调用NonfairSync实现
&& acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
//当前执行线程
final Thread current = Thread.currentThread();
int c = getState();
//state为0,说明当前资源没有被锁定
if (c == 0) {
//CAS修改state为1,上锁
if (compareAndSetState(0, acquires)) {
//设置当前线程为独占
setExclusiveOwnerThread(current);
return true;
}
}
//state不为0,说明当前资源处于锁定状态
//判断当前线程是否为当前资源锁定的独占线程
//如果是,则锁重入
else if (current == getExclusiveOwnerThread()) {
//添加重入次数,修改state,释放锁时也会逐个减少重入次数
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//加锁失败
return false;
}
tryAcquire如果加锁失败,说明当前线程争抢锁资源失败了,那就要加入到阻塞队列(CLH)中去排队等待了。
CLH 队列(Craig, Landin, and Hagersten 队列)是一种用于实现自旋锁的队列数据结构,基于链表实现,旨在解决自旋锁中的一些缺点,比如对缓存一致性问题、内存系统的高度竞争等。CLH 队列的核心思想是使用一组简单的节点构成链表,每个节点都维护一个布尔类型的标志位,表示该节点的前一个节点是否仍然在使用锁。这样,获取锁的线程可以根据前一个节点的标志位来判断自己是否能够进入临界区。这种方式减少了对共享变量的竞争,从而在多处理器系统中降低了缓存一致性流量。
acquireQueued(addWaiter(Node.EXCLUSIVE), arg) //执行acquireQueued先进行入队
CLH结构如下,prev为前继节点,AQS在此基础上加入了后继节点next,组成双向链表。由此构成了一个FIFO队列,用于记录等待锁线程的顺序。
/*
* <pre>
* +------+ prev +-----+ +-----+
* head | | <---- | | <---- | | tail
* +------+ +-----+ +-----+
* </pre>
*/
//队列节点
static final class 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;
//valatile来保证内存可见性,CAS来保证原子性
//节点等待状态,值为上述列举CANCELLED、SIGNAL、CONDITION、PROPAGATE
//当前节点的waitStatus表示后继节点状态
volatile int waitStatus;
//前驱指针
volatile Node prev;
//后继指针
volatile Node next;
//节点线程
volatile Thread thread;
}
private Node addWaiter(Node mode) {
//创建node节点,绑定当前线程 mode为独占
Node node = new Node(Thread.currentThread(), mode);
//获取tail,tail为队尾指针
Node pred = tail;
//如果pred不为空,说明队列不为空
if (pred != null) {
//将当前指针前驱指针指向pred即尾部指针
node.prev = pred;
//入队要保证原子性,即通过CAS将node设置为tail
if (compareAndSetTail(pred, node)) {
//入队成功则将原tail(pred)后继指针指向新tail(node)
pred.next = node;
return node;
}
}
enq(node);
return node;
}
tail为空,说明队列未初始化,enq方法实现初始化入队
private Node enq(final Node node) {
//自旋直到入队成功
for (;;) {
Node t = tail;
if (t == null) {
//初始化队列,初始化Head需要保证原子性
if (compareAndSetHead(new Node()))
//将tail指向head
tail = head;
} else {
//初始化完成后再入队
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
到这里就入队完成了,则执行acquireQueued
实现入队的线程阻塞中断处理
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//自旋处理
for (;;) {
//predecessor方法用来获取当前节点的前驱节点
final Node p = node.predecessor();
//这里解释一下,队头是head.next,并不是head,因为head只作为标记节点只起到辅助作用,不存储线程
//node前驱节点如果是head,说明node是队头,则再次尝试去获锁
if (p == head && tryAcquire(arg)) {
//获锁成功,将node设置为head
setHead(node);
//释放前head节点,gc回收
p.next = null;
failed = false;
//中断标记为false
return interrupted;
}
//如果非队头或是队头但获锁失败,则需要阻塞当前线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire 用来判断获取同步锁失败后,当前线程是否应该被阻塞(park),主要修改waitStatus状态
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//waitStatus 表示当前节点后继节点的状态
//获取node前驱节点ws
int ws = pred.waitStatus;
//如果ws==SIGNAL,说明node等待被唤醒去争锁资源
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
//ws大于0,表示当前节点待清除
do {
//从队列中清除所有ws为2的节点,注意这里的当前节点为pred,
//CONDITION状态作用于自身节点,和SIGNAL不一样
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//将ws设置为SIGNAL,此处的ws为初始化状态0
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
当node waitStatus状态设置成功后,则调用parkAndCheckInterrupt方法实现阻塞
private final boolean parkAndCheckInterrupt() {
//阻塞当前线程,直到获取锁的线程执行unlock唤醒当前线程,才会接着往下执行
LockSupport.park(this);
//清除中断标志并返回当前中断标志,如果开发者在获锁前设置了线程中断,则此处会返回true,并且park不会阻塞
//因为线程中断实现了unPark,unPark用来唤醒park,就算unPark先执行,park也会响应
//所以才要清除中断标志
//自旋下次进来时,中断标志已经被清除,则线程会park住
//当且仅当 当前node线程为队头并且获锁成功,此时中断标记为true,会响应中断
return Thread.interrupted();
}
回到acquire
方法,如果中断标志为true,需要重新设置当前线程中断,待开发者业务响应处理
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
3.解锁
reentrantLock.unlock();
public void unlock() {
sync.release(1);
}
tryRelease方法用于实现释放同步状态,由子类实现。即ReentrantLock
成员类Sync
,具体实现如下
protected final boolean tryRelease(int releases) {
//加锁时有说明,此处减去重入次数
int c = getState() - releases;
//如果当前线程不是独占线程
if (Thread.currentThread() != getExclusiveOwnerThread())
//监视器状态异常
throw new IllegalMonitorStateException();
boolean free = false;
//当c等于0时
if (c == 0) {
//释放锁资源成功
free = true;
//清除线程独占
setExclusiveOwnerThread(null);
}
//设置state,此时处于锁定独占状态,不需要通过CAS设置
setState(c);
return free;
}
public final boolean release(int arg) {
//释放锁成功
if (tryRelease(arg)) {
Node h = head;
//队列不为空,且waitStatus已经设置为SIGNAL
if (h != null && h.waitStatus != 0)
//唤醒队头节点,即head.next节点
//head.next 节点会在park地方醒来继续去争抢锁资源
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
//CAS设置ws=0,清除节点等待状态
compareAndSetWaitStatus(node, ws, 0);
//如果队头为空或为待清除节点,则从尾部遍历找到可用节点
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//唤醒s节点
LockSupport.unpark(s.thread);
}
到这里,整个加锁接解锁逻辑就结束啦,撒花庆祝!!!
要点说明
上述是通过非公平锁NonfairSync
深入源码分析的,公平锁FairSync
获取锁逻辑也提一下
公平锁和非公平锁区别上边有提到过,简单理解这是两种不同策略
公平锁获取锁的顺序和发出获取锁请求的顺序一致。即先发出获锁请求的线程先获锁。而非公平锁,线程获取锁的顺序不确定,可能后来线程有限获取锁,排在队头的线程不一定能争抢到锁资源
所以公平锁FairSync
实现公平的逻辑就是hasQueuedPredecessors方法,简单来说这个方法就是去找阻塞队列里有没有线程正在排队,如果有,那么不好意思,乖乖去排队吧
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;
}
public final boolean hasQueuedPredecessors() {
//获取队头、队尾
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
//head != tail说明队列不为空
//既然队伍不为空,那为什么 h.next == null呢,是因为队列还未初始化完成,CAS设置头节点时,tail还未指向head,即head != tail且h.next == null
//h.next == null 成立说明队列为空
//当队列不为空时,判断队头节点线程是否为当前释放锁线程,如果不是则当前线程需要去排队
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}