AbstractQueuedSynchronizer
AQS:AbstractQueuedSynchronizer(字面翻译 抽象的 排队的 同步器,即同步器框架)
AQS定义了多线程下,访问共享资源的同步器框架
AQS具备的特性
- 阻塞等待队列
- 共享/独占
- 公平/非公平
- 可重入
- 允许中断
初识AbstractQueuedSynchronizer之ReentrantLock
想象一下如果自己要实现一个锁机制,要能做到什么?
同一时间,只有一个线程能访问到公共资源的功能。持有线程操作完以后,其他线程继续竞争
如果不用synchronized,如何实现呢?
实现步骤
- 创建一个对象,有两个方法,加锁、解锁
- 对象中有如下属性:
- state(状态):记录当前是否有线程占用锁
- 队列:没有竞争到锁的对象放到队列中
- Lock时修改state状态(要保证只有一个线程能修改成功)
- 没有修改成功的线程放到队列中,并线程阻塞
- unlock时,修改状态,并从队列中取出一个线程解除阻塞,并去获取锁
开始学习ReentrantLock
通过上面分析就是ReentrantLock实现锁机制的大体流程,为了实现这些,用到了:
- compareAndSwapInt: 多线程修改对象中状态时,保证只有一个线程能修改成功
- for ( ; ; ) {//业务} :自旋,保证多线程入队的时候,最终入队成功
- head、tail:头节点与尾节点,队列的开始位置与结束位置
- LockSupport.park(this):阻塞线程
- LockSupport.unpark(s.thread):释放阻塞
- Node节点中的waitStatus属性:记录线程状态(0-初始化、CANCELLED-中断或者取消、SIGNAL-可竞争锁),根据此状态判断队列中的线程是否健康的,是否可以竞争锁 还是抛弃
ReentrantLock实现流程
粉色代码是ReentrantLock代码,绿色代码是AbstractQueuedSynchronizer代码
代码分析
lock方法
- tryAcquire:尝试获取锁
- addWaiter:未获取到锁,添加到队列中
- acquireQueued:自旋【cas获取锁+阻塞线程(等待唤醒)】
tryAcquire:尝试获取锁**
/**
* 尝试获取锁
*
* 判断是否有线程持有锁,如果state=0.表示没有线程持有锁
* 没有线程持有锁的话,判断队列是否有数据,如果等待队列中没有数据,尝试CAS方式修改state(抢锁)
* 如果设置状态成功(抢锁成功),设置持有锁对象为当前线程
* 如果当前有线程持有锁,判断是否是当前线程持有的
* 如果是当前线程持有锁,修改获取锁次数(单线程操作,不会有并发问题,不需要CAS)
**/
@Override
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//hasQueuedPredecessors判断队列是否有数据
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:当前线程放到队列中(独占模式)
/***
* 根据当前线程创建一个node节点,并添加到CLH队尾
*
* 将当前线程包装成node对象,并设置成独占模式
* 判断队列中是否有数据:取出队尾,如果队尾不是空,表示有数据,尝试对象入队尾
* 将当前节点的前驱节点指向队尾,并使用CAS操作更新属性
* 如果设置失败,使用enq自旋方式一直尝试入队
**/
private Node addWaiter(Node mode) {
// 将当前线程包装成node对象,并设置成独占模式
Node node = new Node(Thread.currentThread(), mode);
// 取出队尾节点
Node pred = tail;
// 队尾不是空
if (pred != null) {
// 当前节点前驱节点为队尾节点
node.prev = pred;
// CAS方式更新字段
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 入队失败,自旋方式一直入队,直到入队成功
enq(node);
return node;
}
enq:自旋+cas入队,直到入队成功
/***
* 将节点添加到CLH同步等待队列中
*
* 自旋 一直尝试往队列中追加,直到入队成功
* 队列无数据时,需初始化,使用CAS操作,初始化head节点,并设置头尾同指向同一节点
* 进入第二次循环,将当前节点前驱点设置成尾节点
* 使用CAS操作设置tail节点为当前节点(入队尾)
* 返回尾节点
**/
private Node enq(final Node node) {
for (;;) {
// 取队尾数据
Node t = tail;
// 队尾是空,队列无数据
if (t == null) {
// 初始化头部节点
if (compareAndSetHead(new Node())) {
// 第一次初始化的时候,头部和尾部一样
tail = head;
}
} else {
// 如果已经初始化过了,将当前节点插入队尾
// 当前节点的前驱节点设置成尾节点,表示插入CLH队尾
node.prev = t;
// 将当前节点设置为tail 尾部节点
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
acquireQueued:准备阻塞线程
/***
* 自旋+cas获取锁+阻塞
*
* 判断当前节点的前驱节点如果是头部节点,表示当前线程是队列中的第一个,再尝试获取下锁
* 如果前驱节点不是头部节点,判断前驱节点状态,如果前驱节点
* 将当前线程的节点的前驱节点设置成健康的节点,
* 并设置前驱节点状态为signal
* 阻塞当前线程
**/
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;
failed = false;
// 返回失败
return interrupted;
}
// 如果当前节点可以接收信号,阻塞当前线程
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
interrupted = true;
}
}
} finally {
if (failed) {
cancelAcquire(node);
}
}
}
unLock方法
- tryRelease:尝试释放锁
- unparkSuccessor:如果当前线程持有锁全部释放,唤醒队列中第一个节点
/***
* 释放锁
* 状态-释放锁数量
* 如果非当前线程操作释放锁,报错
* 如果当前线程的锁都释放完了,清空线程记录
* 如果全部释放了,返回true,否则false
**/
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread()) {
throw new IllegalMonitorStateException();
}
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
/***
* 唤醒线程
*
* 取节点的状态,判断状态是否正常,为SIGNAL则设置状态为0
* 取节点的下一个节点,如果下一个节点状态是无效的,
* 则从队尾一直往前取,取到离当前节点最近的健康的节点
* 用取出的节点,唤醒此节点对应的线程
**/
/***
* 唤醒线程 TODO
*
* 判断队列中头节点状态是否是可以接收信号状态
* 如果是,使用CAS修改为0
* 取出队列中第一个节点唤醒此节点
*
**/
private void unparkSuccessor(Node node) {
// 节点释放阻塞
// 判断节点状态,如果小于0,节点设置成0,
int ws = node.waitStatus;
if (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) {
// 唤醒节点
LockSupport.unpark(s.thread);
}
}