JUC并发编程基础AQS
程序员
勾三
(J3)个人简历:
j3code.cn
1、Lock使用案例
前提:改源码环境为JDK11
lock.lock();
try {
if (getNumber() > 0) {
this.number--;
System.out.println(Thread.currentThread().getName() + ",买到一张车票,当前还剩车票数量:" + getNumber());
}
} finally {
//资源关闭
lock.unlock();
}
2、分析锁的创建流程
Lock lock = new ReentrantLock();
ReentrantLock创建
private final Sync sync;
public ReentrantLock() {
sync = new NonfairSync();
}
Sync同步对象
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
// ...
}
Sync同步对象子类
NonfairSync
非公平锁
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
// ...
}
FairSync
公平锁
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
// ...
}
2.1 图解 ReentrantLock 类继承关系图
3、分析加锁流程
关键代码
lock.lock();
1、java.util.concurrent.locks.ReentrantLock#lock
public void lock() {
// 通过 Sync 对象去获得一个资源
sync.acquire(1);
}
2、java.util.concurrent.locks.AbstractQueuedSynchronizer#acquire
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
3.1 tryAcquire
尝试获取资源(锁)
1)java.util.concurrent.locks.ReentrantLock.NonfairSync#tryAcquire
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
2)java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire
final boolean nonfairTryAcquire(int acquires) {
// 获得当前线程
final Thread current = Thread.currentThread();
// 拿到锁的 state(这是没一把锁都默认拥有的,默认值 0),
int c = getState();
if (c == 0) {
// acquires = 1,State 修改成功
if (compareAndSetState(0, acquires)) {
// 放心加锁
setExclusiveOwnerThread(current);
// 返回加锁成功!
return true;
}
}
// 比较当前加锁线程,和已经获取了锁的线程笔记
else if (current == getExclusiveOwnerThread()) {
// 代表同一个线程加锁,简单说就是重入
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 设置加锁次数,这里没有线程安全问题
setState(nextc);
// 返回加锁成功!
return true;
}
// 失败
return false;
}
3.2 tryAcquire流程总结
- 获取当前线程
- 获取当前锁的状态:state,默认为 0
- 判断是否被加锁,就是 state 不为 0 的时候
- 第一种:没有加锁,调用这个方法 compareAndSetState,修改 state 的值将 0 变为 1,接着讲属性 exclusiveOwnerThread 的值赋为当前线程,至此当前线程加锁成功!
- 第二种:加锁,判断加锁的线程是否为当前线程,如果是则表示重入,将 state 的值累加就行,否则就是加锁失败
3.3 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
3.2.1 addWaiter(Node.EXCLUSIVE)
1)java.util.concurrent.locks.AbstractQueuedSynchronizer#addWaiter
private Node addWaiter(Node mode) {
// 创建node节点
Node node = new Node(mode);
for (;;) {
// 将尾指针赋值,尾指针默认null
Node oldTail = tail;
if (oldTail != null) {
// 将 node 的前指针指向 oldTail
node.setPrevRelaxed(oldTail);
// 设置尾指针,指向 node
if (compareAndSetTail(oldTail, node)) {
// 将 oldTail 的后指针指向 node
oldTail.next = node;
// 返回 node
return node;
}
} else {
// 第一次尾指针为null,初始化同步队列
initializeSyncQueue();
}
}
}
private final void initializeSyncQueue() {
Node h;
// 初始化头节点
if (HEAD.compareAndSet(this, null, (h = new Node())))
// 初始化尾节点
tail = h;
}
3.2.2 acquireQueued(Node, arg))
1)java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireQueued
final boolean acquireQueued(final Node node, int arg) {
// 响应中断标志位,默认false
boolean interrupted = false;
try {
for (;;) {
// 获取 node 的前指针对象
final Node p = node.predecessor();
// 判断 node 的前指针是否为头指针,如果是,再次尝试获取锁资源
// 注意,,,,,,只有当阻塞队列仅有一个节点的时候才会去尝试获取资源
if (p == head && tryAcquire(arg)) {
// 获取到资源了,将头尾指针的地址重新指向 new Node()
setHead(node);
// 将无用节点置空,方便垃圾回收
p.next = null; // help GC
// 返回中断标志位
return interrupted;
}
// 未获取到锁资源,则应该阻塞,之后判断响应中断
if (shouldParkAfterFailedAcquire(p, node))
// 阻塞当前线程,并且获取中断响应标志
// 如果 false 与 false = false 如果 false 与 true = true
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
if (interrupted)
selfInterrupt();
throw t;
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取前指针节点的状态 waitStatus 的值
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
// 将同步队列中节点大于 1 的 node 给踢出同步队列(waitStatus = 1 表示该节点无效)
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
// 将 pred 节点中 waitStatus 的值由 ws = 0 改为 Node.SIGNAL = -1
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
}
return false;
}
// 阻塞,并检查中断
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
3.2.3 总结acquireQueued方法
1、封装当前获取锁的线程,封装成一个 Node 节点
2、将封装的节点,加入到同步队列,并且分以下几个情况:
- 第一种:同步队列为空,那么 head、tail 指针同时指向该 Node ,并且当前线程会再次去尝试获取锁资源,如果获取到锁,那么同步队列将再次重置未一个空队列。
- 第二种:同步队列不空,那么该线程则没有再一次获取锁资源的机会,进入下一个流程
3、自旋及修改该节点的前一个节点的 waitStatus 值为 -1 表示该节点可以被唤醒获取资源,然后将当前线程阻塞,等待下一次被唤醒。
4、在将当前节点置为 -1 的过程中,如果前一个Node 的 waitStatus > 0 那么会触发踢出同步队列中 Node 节点waitStatus = 1 的无效节点。
3.4 图解加锁流程
4、释放锁流程
代码切入点
lock.unlock()
1、java.util.concurrent.locks.ReentrantLock#unlock
public void unlock() {
// 释放资源 1 次
sync.release(1);
}
2、java.util.concurrent.locks.AbstractQueuedSynchronizer#release
public final boolean release(int arg) {
// 尝试释放资源
if (tryRelease(arg)) {
// 拿到同步队列头节点
Node h = head;
// 判断同步队列不为空,并且同步队列中头节点指向的 Node 节点中 waitStatus 值不为 0 才回去唤醒
if (h != null && h.waitStatus != 0)
// 真正去唤醒代码
unparkSuccessor(h);
return true;
}
return false;
}
3、java.util.concurrent.locks.ReentrantLock.Sync#tryRelease
protected final boolean tryRelease(int releases) {
// 获取锁的 state ,并减去要释放锁的次数 releases
int c = getState() - releases;
// 判断当前获取锁的线程是否为当前要释放锁的线程
if (Thread.currentThread() != getExclusiveOwnerThread())
// 两者不一致,报错
throw new IllegalMonitorStateException();
// 两者一致
boolean free = false;
// 判断释放锁之后,判断 state 是否为 0 无所状态,这是判断重入
if (c == 0) {
// state = 0 ,彻底释放锁
free = true;
setExclusiveOwnerThread(null);
}
// 重入,那么仍然占有锁资源,并且将状态值再次设置给 state
setState(c);
// 返回 false(重入)
return free;
}
4、java.util.concurrent.locks.AbstractQueuedSynchronizer#unparkSuccessor
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
node.compareAndSetWaitStatus(ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
// 下面这部分逻辑就是找到要唤醒的线程,我这里可以理解为就是离 head 节点最近的一个 waitStatis 不大于 0 的节点
s = null;
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
if (s != null)
// 唤醒
LockSupport.unpark(s.thread);
}
唤醒之后,会来到下面这段代码,继续执行:
// 阻塞,并检查中断
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
// 唤醒之后,会继续下面的代码流程
return Thread.interrupted();
}
4.1 总结
- 先尝试去释放锁一次,这里考虑了重入。
- 如果释放锁一次之后,state = 0 ,那么将占有锁的线程置为 null,反之则只改变 state 的值(-1)。
- 如果释放锁成功,那么继续判断同步队列是否有等待的 Node 节点线程,如果没有则直接返回,反之则有其他线程被阻塞在同步队列中,需要唤醒。
找到要唤醒的线程节点,我这里可以理解为就是离 head 节点最近的一个 waitStatis 不大于 0 的节点,并且触发提出节点状态(waitStatis)的值大于 0 的节点,因为它是无效的,所以要踢出掉
。- 被唤醒的线程会在 parkAndCheckInterrupt() 方法中醒来,并继续抢占锁资源
4.2 图解释放锁流程
5、锁的阻塞使用案例
Condition 使用案例
public class ConditionDemo {
private Lock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition();
public static void main(String[] args) throws Exception {
ConditionDemo conditionDemo = new ConditionDemo();
System.out.println(Thread.currentThread().getName() + "开始执行....");
new Thread(() -> {
conditionDemo.lock.lock();
try {
try {
System.out.println(Thread.currentThread().getName() + "我要开始等待了.....");
conditionDemo.conditionA.await();
System.out.println(Thread.currentThread().getName() + "我等待结束了.....");
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
//资源关闭
conditionDemo.lock.unlock();
}
}, "J3,快来关注我-").start();
Thread.sleep(2 * 1000);
System.out.println(Thread.currentThread().getName() + "主线程要开始唤醒其他线程了.....");
conditionDemo.lock.lock();
try {
conditionDemo.conditionA.signal();
}finally {
//资源关闭
conditionDemo.lock.unlock();
}
Thread.sleep(2 * 1000);
System.out.println(Thread.currentThread().getName() + "主线程结束.....");
}
}
6、锁的条件等待流程
代码
conditionDemo.conditionA.await();
1、java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject#await()
public final void await() throws InterruptedException {
// 判断中断标志位,响应中断
if (Thread.interrupted())
throw new InterruptedException();
// 生成条件等待队列的节点,并且初始化条件队列,并将节点放入条件队列
Node node = addConditionWaiter();
// 完全释放锁,为什么说是完全,因为有重入的可能,所以,你释放了多少,在下一次拿到CPU资源的时候,就要还原到释放前的加锁次数
int savedState = fullyRelease(node);
// 中断响应次数
int interruptMode = 0;
// 循环判断,node 节点是否在 同步队列里面
while (!isOnSyncQueue(node)) {
// 不在同步队列里面,那么将该线程阻塞
LockSupport.park(this);
// 被唤醒之后(被唤醒的节点,是会别调用唤醒逻辑的线程放入同步队列中),判断是否响应中断
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 这里是说明这个线程已经在同步队列里面了,那么就只要按部就班的执行获取锁的流程即可(注意,这里的那拿锁次数是,savedState)
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 去除条件队列中,waitStatus = -2 的节点
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
// 响应中断
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
2、java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject#addConditionWaiter
private Node addConditionWaiter() {
// 判断是否是持有锁的线程过来生成条件队列中的节点
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 获取条件队列中的尾节点
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
// 这里最先触发一次清除条件队列中,waitStatus = -2 的节点
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
// 找到最后一个 waitStatus 不为 -2 的尾节点
t = lastWaiter;
}
// 生成条件队列中的节点
Node node = new Node(Node.CONDITION);
// 将节点放入条件队列中,并将指向关系给生成好
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
// 返回条件队列中的节点
return node;
}
6.1 总结
1、判断是否响应中断
2、生成条件等待队列的节点,并且初始化条件队列,并将节点放入条件队列
3、完全释放锁,为什么说是完全,因为有重入的可能,所以,你释放了多少,在下一次拿到CPU资源的时候,就要还原到释放前的加锁次数
4、循环判断节点是否在同步队列中,如果在,则跳过阻塞,直接去竞争锁资源,反之阻塞,等待唤醒。
5、如果唤醒,则跳出循环,竞争锁资源,如果争抢到锁资源后,会移除条件队列
中Node 属性 waitStatus = -2 的节点,最后才是完成一次 await 流程。
6.2 图解锁阻塞等待流程
7、锁的唤醒流程
代码
conditionDemo.conditionA.signal()
1、java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject#signal
public final void signal() {
// 判断执行唤醒逻辑的线程是否时当前持有锁的线程
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 先拿到条件队列中头指针
Node first = firstWaiter;
// 条件队列不为空,则去执行唤醒的动作
if (first != null)
// 开始去唤醒线程
doSignal(first);
}
2、java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject#doSignal
private void doSignal(Node first) {
do {
// 如果条件队列中的节点只有一个,那么会复位条件队列中的头尾指针
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
// 转移唤醒
} while (!transferForSignal(first) &&
// 转移唤醒失败,继续转移唤醒下一个节点,知道同步队列末尾
(first = firstWaiter) != null);
}
3、java.util.concurrent.locks.AbstractQueuedSynchronizer#transferForSignal
final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
// 设置 Node 节点状态,从条件队列中移到同步队列中,肯定是要把条件队列中的标识给移除掉
if (!node.compareAndSetWaitStatus(Node.CONDITION, 0))
return false;
/*
* Splice onto queue and try to set waitStatus of predecessor to
* indicate that thread is (probably) waiting. If cancelled or
* attempt to set waitStatus fails, wake up to resync (in which
* case the waitStatus can be transiently and harmlessly wrong).
*/
// 将 node 节点放入到同步队列尾部,然后返回同步队列中倒数第二个节点
Node p = enq(node);
int ws = p.waitStatus;
// ws = 0,然后将 同步队列中倒数第二个节点的状态由 0 改为 -1
if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL))
// 如果修改失败,那么就把阻塞的线程唤醒,让它去争取锁,顺便检查一下同步队列中的各个节点的状态是否合理
LockSupport.unpark(node.thread);
// 唤醒成功!
return true;
}
7.1 总结
1、先判断执行唤醒逻辑的线程是否时当前持有锁的线程
2、判断条件队列不为空,则去执行唤醒的动作
3、真正转移唤醒
- 设置 Node 节点状态,从条件队列中移到同步队列中,肯定是要把条件队列中的标识给移除掉
- 将 node 节点放入到同步队列尾部,然后返回同步队列中倒数第二个节点
- 然后将同步队列中倒数第二个节点的状态由 0 改为 -1
- 第一种修改失败,则唤醒等待的线程,去竞争锁资源
- 第二种修改成功,则代表唤醒线程成功,结束流程
7.2 图解锁唤醒流程
8、AQS总图
本片文章中所涉及的所有流程图可访问下面地址获取
链接:https://gitmind.cn/app/flowchart/95a4909721
密码:6284
好了,今天的内容到这里就结束了,我是 【J3】关注我,我们下期见
。
-
由于博主才疏学浅,难免会有纰漏,假如你发现了错误或偏见的地方,还望留言给我指出来,我会对其加以修正。
-
如果你觉得文章还不错,你的转发、分享、点赞、留言就是对我最大的鼓励。
-
感谢您的阅读,十分欢迎并感谢您的关注。