tryAcquireShared(int); // 尝试以共享方式获取锁,失败返回负数,只能获取一次返回0,否则返回个数
tryReleaseShared(int); // 尝试释放共享锁,可获取返回true,否则false
isHeldExclusively(); // 判断线程是否独占资源
复制代码
如实现类只需实现独占锁/共享锁功能,可只实现tryAcquire/tryRelease
或tryAcquireShared/tryReleaseShared
。虽然实现tryAcquire/tryRelease
可自行设定逻辑,但建议使用state
方法对state
变量进行操作以实现同步类。
如下是一个简单的同步锁实现示例:
public class Mutex extends AbstractQueuedSynchronizer {
@Override
public boolean tryAcquire(int arg) {
return compareAndSetState(0, 1);
}
@Override
public boolean tryRelease(int arg) {
return compareAndSetState(1, 0);
}
public static void main(String[] args) {
final Mutex mutex = new Mutex();
new Thread(() -> {
System.out.println(“thread1 acquire mutex”);
mutex.acquire(1);
// 获取资源后sleep保持
try {
TimeUnit.SECONDS.sleep(5);
} catch(InterruptedException ignore) {
}
mutex.release(1);
System.out.println(“thread1 release mutex”);
}).start();
new Thread(() -> {
// 保证线程2在线程1启动后执行
try {
TimeUnit.SECONDS.sleep(1);
} catch(InterruptedException ignore) {
}
// 等待线程1 sleep结束释放资源
mutex.acquire(1);
System.out.println(“thread2 acquire mutex”);
mutex.release(1);
}).start()
}
}
复制代码
示例代码简单通过AQS
实现一个互斥操作,线程1获取mutex
后,线程2的acquire
陷入阻塞,直到线程1释放。其中tryAcquire/acquire/tryRelease/release
的arg
参数可按实现逻辑自定义传入值,无具体要求。
@param arg the acquire argument. This value is conveyed to {@link #tryAcquire} but is otherwise uninterpreted and can represent anyting you like.
AQS核心结构
Node
前文提到,在AQS
中如果线程获取资源失败,会包装成一个节点挂载到CLH
队列上,AQS
中定义了Node
类用于包装线程。
Node
主要包含5个核心字段:
-
waitStatus
:当前节点状态,该字段共有5种取值: -
CANCELLED = 1
。节点引用线程由于等待超时或被打断时的状态。 -
SIGNAL = -1
。后继节点线程需要被唤醒时的当前节点状态。当队列中加入后继节点被挂起(block)
时,其前驱节点会被设置为SIGNAL
状态,表示该节点需要被唤醒。 -
CONDITION = -2
。当节点线程进入condition
队列时的状态。(见ConditionObject
) -
PROPAGATE = -3
。仅在释放共享锁releaseShared
时对头节点使用。(见共享锁分析) -
0
。节点初始化时的状态。 -
prev
:前驱节点。 -
next
:后继节点。 -
`thr
《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》
【docs.qq.com/doc/DSmxTbFJ1cmN1R2dB】 完整内容开源分享
ead`:引用线程,头节点不包含线程。
nextWaiter
:condition
条件队列。(见ConditionObject
)
独占锁分析
acquire
public final void acquire(int arg) {
// tryAcquire需实现类处理
// 如获取资源成功,直接返回
if (!tryAcquire(arg) &&
// 如获取资源失败,将线程包装为Node添加到队列中阻塞等待
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 如阻塞线程被打断
selfInterrupt();
}
复制代码
acquire
核心为tryAcquire
、addWaiter
和acquireQueued
三个函数,其中tryAcquire
需具体类实现。 每当线程调用acquire
时都首先会调用tryAcquire
,失败后才会挂载到队列,因此acquire
实现默认为非公平锁。
addWaiter
将线程包装为独占节点,尾插式加入到队列中,如队列为空,则会添加一个空的头节点。值得注意的是addWaiter
中的enq
方法,通过CAS+自旋
的方式处理尾节点添加冲突。
acquireQueue
在线程节点加入队列后判断是否可再次尝试获取资源,如不能获取则将其前驱节点标志为SIGNAL
状态(表示其需要被unpark
唤醒)后,则通过park
进入阻塞状态。
参照流程图,acquireQueued
方法核心逻辑为for(;;)
和shouldParkAfterFailedAcquire
。tail
节点默认初始状态为0,当新节点被挂载到队列后,将其前驱即原tail
节点状态设为SIGNAL
,表示该节点需要被唤醒,返回true
后即被park
陷入阻塞。for
循环直到节点前驱为head
后才尝试进行资源获取。
release
release
流程较为简单,尝试释放成功后,即从头结点开始唤醒其后继节点,如后继节点被取消,则转为从尾部开始找阻塞的节点将其唤醒。阻塞节点被唤醒后,即进入acquireQueued
中的for(;;)
循环开始新一轮的资源竞争。
共享锁分析
acquireShared & releaseShared
public final void acquireShared(int arg) {
// 负数表示获取共享锁失败,不同于tryAcquire的bool返回
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
复制代码
acquireShared
和releaseShared
整体流程与独占锁类似,tryAcquireShared
获取失败后以Node.SHARED
挂载到队尾阻塞,直到队头节点将其唤醒。在doAcquireShared
与独占锁不同的是,由于共享锁是可以被多个线程获取的,因此在首个阻塞节点被唤醒后,会通过setHeadAndPropagate
传递唤醒后续的阻塞节点。
// doAcquireShared核心代码
final Node node = addWaiter(Node.SHARED);
…
for (;😉 {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
// r>=0 表示获取锁成功,调整头结点并传递唤醒
setHeadAndPropagate(node, r);
}
}
…
}
复制代码
setHeadAndPropagate
和doReleaseShared
构成共享锁唤醒的核心逻辑。
这两方法的逻辑较为简单,不再进行展开,主要对setheadAndPropagate
的多节点唤醒判断逻辑做出分析。
进入setHeadAndPropagate
,首先需要明确的是,该函数的传入参数propagate
一定是非负数,接下来其唤醒主要为两个判断逻辑:
-
如果
propagate > 0
,表示存在多个共享锁可以获取,可直接进行doReleaseShared
唤醒阻塞节点。 -
如果
propagate = 0
,表示仅当前节点可被唤醒,则有两种情况: -
h == null || h.waitStatus < 0
,通常情况下h != null
,现给出h.waitStatus < 0
的场景。
(h = head) == null || h.waitStatus < 0
的场景执行序列如下:
独占锁共享锁小结