AQS系列文章目录
第一章 AQS学习之ReentrantLock源码解析
前言
通过源码解析 , 了解ReentrantLock的实现过程 . 从实际运用入手 , 一步一步跟踪源码 , 看每一步在源码中是怎么实现的 .
一、AQS是什么?
AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架。
-
CLH队列
-
节点入队列过程
-
初始状态
-
第一个节点入队
-
第二个节点入队
-
…
-
二、使用步骤
1.先看一波ReentrantLock实际运用中是怎么操作的
代码如下(示例):
public class TestAqs {
//1.定义一个ReentrantLock
private final Lock lock = new ReentrantLock();
private int num;
public int addNum(int n) {
//2.上锁 此行代码必须在第一行
lock.lock();
try {
//3.需要自行同步的代码块
num += n;
} finally {
//4.释放锁
lock.unlock();
}
return num;
}
}
2.通过跟踪代码观察上锁的过程
此处的上锁(自旋+CAS)区别于synchronized
首先从lock()方法跟踪:
public class ReentrantLock implements Lock, java.io.Serializable {
//无参构造器
public ReentrantLock() {
sync = new NonfairSync();
}
//1.如果锁目前未被其他线程占用则立即返回
//2.如果这个锁被当前线程持有,则把持有的数量(state)加1,然后立即返回
//3.如果锁被其他线程持有, 则当前线程不会再被线程调度器调用而进入休眠状态,直到获取取到锁,并把数量(state)加1
public void lock() {
sync.lock();
}
}
下面看下NonfairSync.lock方法代码 :
final void lock() {
//尝试直接CAS操作获取锁, 如果失败则通过acquire方法获取
if (compareAndSetState(0, 1))
//如果获取到锁则设置锁被当前线程持有
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
AbstractQueuedSynchronizer.acquire()代码 :
//1.尝试获取锁
//2.不成功的话, 就给当前线程创建一个Node节点, 并把Node节点入队列中排队
//3.入队成功则进行自我中断
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//线程中断
selfInterrupt();
}
尝试获取锁tryAcquire()代码 :
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
//非公平方式获取锁
final boolean nonfairTryAcquire(int acquires) {
//当前线程
final Thread current = Thread.currentThread();
//获取当前资源数 , state = 0说明锁未被占用 , 反之说明锁已经被占用
int c = getState();
//1.c == 0说明锁未被占用 , 尝试通过CAS占用锁 成功则设置锁被当前线程占用并直接返回
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//2.如果锁是被当前线程占用的, 则把state的值加上入参(acquires) 此处说明这是一把可重入锁
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//3.其他情况说明当前线程当前不能获取到锁, 直接返回fasle
return false;
}
假设上一步尝试获取锁tryAcquire()没有成功 , 则要1.给当前线程创建一个Node节点, 将创建的Node节点入队列排队(addWaiter())) 2.为节点找到一个安全的停靠点(acquireQueued()) 看这两个方法的代码:
//1.为当前线程创建Node节点 2.将Node节点入队列
private Node addWaiter(Node mode) {
//创建节点,并设置独占模式
Node node = new Node(Thread.currentThread(), mode);
//尝试将当前Node节点快速入队 判断当前队列的队尾是不是null值
Node pred = tail;
if (pred != null) {
//如果不是null值,则设置当前Node节点的前节点是当前等待队列的队尾
node.prev = pred;
//尝试把当前Node节点置为队尾
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果快速入队没成功, 则通过enq()方法入队
enq(node);
return node;
}
//通过自旋+CAS操作讲当前Node节点入队等待
private Node enq(final Node node) {
//自旋
for (;;) {
//获取当前等待队列的尾节点
Node t = tail;
//如果当前等待队列的队尾为null, 则给设置一个空节点队尾
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
//如果队尾不为空,则设置当前节点(要插入的节点)的前节点为当前等待队列的尾节点
node.prev = t;
//CAS操作将当前节点(要插入的节点)设置为队尾 成功则设置之前的队尾节点的下一个等待节点为当前节点(要插入的节点), 返回; 不成功的会一直自旋, 直到插入队尾成功
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
//1.尝试获取资源
//2.不成功的话,则给当前节点找一个合适的位置等待,并告诉前置节点执行完了要来唤醒我
//3.找到安全的停靠点后调用park()方法挂起当前线程
//4.返回当前线程是否被中断过
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;
}
//寻找安全点停靠 2.告诉前节点执行完要通知我
if (shouldParkAfterFailedAcquire(p, node) &&
//挂起线程 返回当前线程是否被中断过
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
//经过此方法会给当前节点找到一个安全的停靠点,并设置前节点的状态为-1(告诉前节点执行完唤醒我)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
//判断当前节点的前置节点的状态,如果为-1,则说明它执行完之后会通知后节点,则当前节点就可以安全停靠,直接返回
if (ws == Node.SIGNAL)
return true;
//如果前节点的状态大于0,则说明前面的节点已经取消了, 那就从前节点开始向前查找直到查询到状态小于0的节点为止
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//如果前节点的状态小于等于0,则就把前节点的状态设置为-1(告诉前节点执行完唤醒我)
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//挂起当前线程,返回当前线程是否被中断过
private final boolean parkAndCheckInterrupt() {
//挂起
LockSupport.park(this);
//返回当前线程是否被中断过,此方法会清除中断标记
return Thread.interrupted();
}
3.通过跟踪代码观察释放锁的过程
首先从unlock()方法跟踪:
//释放锁
public void unlock() {
sync.release(1);
}
AbstractQueuedSynchronizer.release()代码:
//1.释放锁 2.唤醒下一节点
public final boolean release(int arg) {
//释放锁
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
//唤醒下一节点
unparkSuccessor(h);
return true;
}
return false;
}
//释放锁
protected final boolean tryRelease(int releases) {
//当前线程持有state数量减去要释放的数量
int c = getState() - releases;
//如果当前持有锁的线程不是当前线程,则抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果当前state数量-要释放的数量为0,则说明当前线程可以释放锁了
if (c == 0) {
free = true;
//设置当前持有锁的线程为空,即释放锁
setExclusiveOwnerThread(null);
}
//设置state数量
setState(c);
return free;
}
为什么此处要判断c == 0 ?
因为这是一个可重入的锁, 当前线程持有锁, 但是state的数量不一定为1, 有可能是2.3.4…, 所以要判断c == 0的时候当前线程才可以真正的把锁释放掉.
AbstractQueuedSynchronizer.unparkSuccessor()代码:
//入参为当前的首节点head
private void unparkSuccessor(Node node) {
//获取当前首节点的状态
int ws = node.waitStatus;
//如果当前首节点的状态小于0, 则设置为0.
if (ws < 0)
//清空节点状态
compareAndSetWaitStatus(node, ws, 0);
//获取入参节点的下一个节点
Node s = node.next;
//判断下一节点是否为空或状态大于0(大于0为取消状态)
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);
}
问题讨论
- 在释放锁的时候, 取head节点的下一节点, 如果下一节点的状态小于0则直接唤醒, 否则从等待队列的尾部向前查找状态小于等于0的非head节点.
那么问题来了, 假如等待队列的长度为10, 我从后往前找第三个是可用的, 然后我把这个节点唤醒, 那从后往前的三个节点和首节点(head)的节点怎么办呢? 如果有可用的节点谁来唤醒?