之前我们已经学习了Synchronized关键字,我们也知道了Synchronized是一种排他锁,现在如果让你来设计一个排他锁,该怎么实现呢?
// 设计思路
// 1.实现锁的数据结构
- 锁状态(status):记录锁的状态,用于标识锁是否被占用.
- 持有者线程(ownerThread):记录当前持有锁的线程。
- 等待队列(queue):用于存储那些无法获取锁而被阻塞的线程,这些线程在锁释放时会被唤醒。
// 2.线程调度和阻塞机制
- 获取锁机制:实现获取锁的方法,在竞争情况下确保只有一个线程能够成功获取锁。
- 释放锁机制:实现释放锁的方法,在线程退出同步代码块时,释放持有的锁,并唤醒等待队列中的线程
- 线程等待和唤醒:确保当线程无法获取锁时能够进入等待状态,而在锁释放时能够唤醒等待的线程。
- 等待队列管理:管理等待获取锁的线程队列,确保等待的线程按照特定规则获得执行机会
// 大致逻辑
1.当多个线程操作共享资源时,必须先获取lock
2.得到lock的线程才能执行逻辑,没得到lock的线程则进入阻塞状态,并且放入等待队列queue
3.当获取到lock的线程完成后,释放锁,并且唤醒queue中的线程去重新获取锁,得到后则继续执行逻辑
其实上述思路就是我们今天的主角ReentrantLock的底层设计思路.
ReentrantLock
ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全,比synchronized灵活,比如它支持手动加锁与解锁,支持加锁的公平性。
使用
使用方法很简单,线程操纵资源类就行。主要方法有两个lock()和unlock()
public class ReentrantLockTest {
private static final ReentrantLock lock = new ReentrantLock(true);
public static void main(String[] args) {
// 加锁
lock.lock();
try {
// 访问临界区资源
// ...
} finally {
// 释放锁
lock.unlock();
}
}
}
底层实现
AQS
ReentrantLock底层是基于AbstractQueuedSynchronizer的,内部有三大属性:head、tail、state
// 队列头部,等待队列中的首节点,它指向第一个正在等待获取锁的线程节点
private transient volatile Node head;
// 队列尾部,等待队列中的尾节点,它指向最后一个正在等待的线程节点
private transient volatile Node tail;
// 同步状态,表示锁的状态,用于记录锁被重入的次数
private volatile int state;
在AQS中, Node对象是用来表示等待队列中的每一个线程节点的,上述的head、tail都为Node对象
// Node对象重要属性
class Node{
// 节点等待状态
/*
CANCELLED (1):表示节点已被取消
SIGNAL (-1):表示后续节点(当前节点的 next 节点)需要被唤醒。
CONDITION (-2):表示节点在等待某个条件。
PROPAGATE (-3):表示 releaseShared 应当传播到其他节点。
0:表示初始状态。
*/
volatile int waitStatus;
// 双向链表当前节点前节点
volatile Node prev;
// 下一个节点
volatile Node next;
// 线程的引用,表示等待队列中的具体线程
volatile Thread thread;
// condition条件等待的下一个节点
Node nextWaiter;
}
lock() 实现
// 以下列公平锁为例,分析lock()底层逻辑
ReentrantLock lock = new ReentrantLock(true);
lock.lock();
// -> FairSync实现lock()
final void lock() {
acquire(1);
}
// -> 进入AQS的acquire(),获取独占锁
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
先直接讲述这几个方法的作用
// tryAcquire(arg) -> 尝试获取锁
// addWaiter(Node.EXCLUSIVE), arg) -> 线程进入等待队列
// acquireQueued() -> 阻塞线程
下面分别讲述方法逻辑
tryAcquire(arg)
尝试获取锁
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取当前锁的状态
int c = getState();
// 如果当前锁状态为0(表示当前没有线程持有锁)
if (c == 0) {
// 如果当前没有等待线程或者获取锁失败,则尝试CAS设置锁状态为acquires
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; // 获取锁失败
}
addWaiter()
若获取锁失败,则会加入等待队列
private Node addWaiter(Node mode) {
// 创建一个新的节点,表示当前线程
Node node = new Node(Thread.currentThread(), mode);
// 尝试快速添加节点到队列尾部(fast path)
Node pred = tail; // 获取队尾节点
if (pred != null) {
node.prev = pred; // 设置新节点的prev为队尾节点
// 尝试使用CAS操作将新节点设置为队尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node; // 设置队尾节点的下一个节点为新节点
return node; // 添加成功,返回新节点
}
}
// 添加节点到队列尾部(full enq)
enq(node); // 调用enq方法,将节点添加到队列尾部
return node; // 返回新节点
}
最开始tail节点一定为null,所以会来到enq(node)
private Node enq(final Node node) {
// 无限循环
for (;;) {
Node t = tail; // 获取队尾节点
if (t == null) { // 如果队尾节点为空,则需要初始化
// 必须进行初始化,尝试使用CAS操作将一个新的头节点设置为队列的头部
if (compareAndSetHead(new Node()))
tail = head; // 初始化尾节点为头节点
} else {
node.prev = t; // 设置新节点的prev为队尾节点
// 尝试使用CAS操作将新节点设置为队尾节点
if (compareAndSetTail(t, node)) {
t.next = node; // 设置队尾节点的下一个节点为新节点
return t; // 返回原始的队尾节点
}
}
}
}
第一次循环:刚开始等待队列是空的,所以会先初始化,初始化出来的队列如下图:head、tail都指向新创建Node节点,waitStatus = 0,thread = null
第二次循环:已经创建好了等待队列,现在需要把代表当前线程的节点入队,如下图:
acquireQueued()
将当前线程构建完Ndoe并放入等待队列后,下面则是去阻塞线程.
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);
// 清空前驱节点的 next 引用,帮助 GC
p.next = null; // help GC
failed = false;
return interrupted;
}
// 如果应该在获取失败后阻塞线程,并且阻塞期间被中断过
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) // 这里会进行阻塞线程
interrupted = true; // 设置中断标志
}
} finally {
// 如果获取锁失败,取消获取(将节点从等待队列中移除)
if (failed)
cancelAcquire(node);
}
}
// 说明
1.第一次进来的时候,会重新尝试获取锁,因为在刚才入队的过程中,持有锁的线程可能已经释放了锁.如果前驱节点是头节点并且尝试获取锁成功,则将当前节点设置为头节点,表示获取锁成功。
2.获取锁失败,则会执行shouldParkAfterFailedAcquire(p, node),这里第一次会将前驱节点的waitStatus设置为-1(SIGNAL),因为持有锁的线程在释放锁的时候,会判断head节点的waitestate是否!=0,如果!=0成立会再把waitstate = -1->0.
3.第一次循环shouldParkAfterFailedAcquire(p, node)返回false,第二次循环才会去阻塞线程parkAndCheckInterrupt().
到这里为止,lock()方法就阻塞了线程,接下来我们来看下唤醒unlock的逻辑.
unlock() 实现
tryRelease(arg) 释放锁
unparkSuccessor(h); 唤醒节点
lock.unlock();
// ->
sync.release(1);
// ->
public final boolean release(int arg) {
// 尝试释放锁
if (tryRelease(arg)) {
// 获取头节点
Node h = head;
// 如果头节点不为空,并且头节点的状态不为0(对应shouldParkAfterFailedAcquire(p, node))
if (h != null && h.waitStatus != 0)
// 唤醒后继节点
unparkSuccessor(h);
return true; // 释放锁成功
}
return false; // 释放锁失败
}
tryRelease() 释放锁
protected final boolean tryRelease(int releases) {
// 计算释放后的新状态值
int c = getState() - releases;
// 如果当前线程不是持有锁的线程,抛出非法监视器状态异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 如果新状态值为0,表示锁被完全释放
if (c == 0) {
free = true; // 标记锁已经被完全释放
setExclusiveOwnerThread(null); // 将持有锁的线程置为null
}
setState(c); // 更新锁的状态值
return free; // 返回锁是否完全释放的标志
}
unparkSuccessor(h) 唤醒节点
// 传入的node为head头节点
private void unparkSuccessor(Node node) {
// 这里又把head的waitStatus从-1改为0
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 获取头节点的下一个节点
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
// 从尾部向前搜索,直到找到一个等待状态不大于0的节点(未被取消的后继节点)
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 如果找到了合适的后继节点,则唤醒其对应的线程
if (s != null)
LockSupport.unpark(s.thread); // 唤醒线程
}
// 这里执行后,之前阻塞线程就会唤醒
LockSupport.unpark(s.thread);
// 这是之前阻塞的代码块
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()) // 1.从这里继续执行,继续走循环
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
总结
下图方便理解
这里也只是针对简单的情况进行描述,将大致的流程描述出来,过程晕晕的,AQS将太多场景柔和在了一个框架中,只能说大牛真牛。