阅读前说明
写的时候比较仓促,好多总结都写在代码里,因为我是一边看代码一边写个人总结,代码执行到这一步承接上下文的作用,还有为什么走到这一步,为什么这样写,而且方法是从执行的顺序由上而下的,总之我都会把我个人心里想法写在代码注释里,写的不好见谅。
建议
要有自己的思路,别跟着我的思路走,我的只适合参考,不是别人说的就是对的,要有自己的思路。
自己摸索代码的执行思路,想象此时的场景,多提些为什么?带这问题去看,方向就很明朗了
ReentrantLock 构造图
sync结构图
1. 构造函数
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
//构建一个非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
//根据传进来的布尔值,最终初始化公平锁还是非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
2. 非公平锁lock 方法
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
//通过cas原理自旋,如果自旋获取锁成功则将上下文线程切换到当前线程
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//否则尝试获取锁
acquire(1);
}
3. compareAndSetState(int expect, int update) 方法
//将当前AbstractQueuedSynchronizer 将同步阻塞队列对应的内存偏量stateOffset位置的值与期望值expect比较(stateOffset为状态偏量),如果相等则将update覆盖对应内存偏量stateOffset位置的值,这里可以看出内存偏量stateOffset位置的值记录上锁状态
// stateOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
//可以理解这里内存偏量stateOffset位置的值实际对应的就是AbstractQueuedSynchronizer中的成员变量state,这里statev 用volatile修饰,防止多线程获取锁状态出现冲突
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
4. acquire(int arg) 方法
public final void acquire(int arg) {
//没有获取到所资源,这个时候会在阻塞队列队尾中添加当前线程的节点,如果前面有等待的节点则将该节点挂起,知道等待前面节点释放资源被唤醒
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
5. 非公平锁tryAcquire方法
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
6. nonfairTryAcquire(int acquires) 方法
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取当前锁状态
int c = getState();
//如果锁状态为0,说明没有被占用没有上锁
if (c == 0) {
//这个时候尝试自旋上锁,上锁成功设置上下文线程为当前线程
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果当前竞争资源的线程为当前线程
else if (current == getExclusiveOwnerThread()) {
//则之前的状态加上现在需要上锁的状态 这里说明ReentrantLock是可重入锁,可以重复上锁,然后重新设置新的锁状态
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
7. addWaiter(Node mode) 方法
总结: 该方法主要以当前线程和mode节点添加到一个阻塞队列当中进行维护等待所资源释放
private Node addWaiter(Node mode) {
//添加一个以当前线程的node节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
//如果当前链表容器尾部数据不为空,则将当前新节点通过自旋成功后,添加到链表尾部
//compareAndSetTail这里就不用解释了,和上方自旋同理
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//到这里,证明自选失败,说明有几个线程都在抢夺锁资源都失败了,需要将其加入到阻塞队列中,等待所资源释放,这里通过调用 enq(node)会无限自旋下去,直到自选成功加入到队列中
enq(node);
return node;
}
8. enq(final Node node) 方法
//无限自旋,自旋成功为止,如果尾部节点为空,则新节点为头部节点同样也是尾部节点,如果尾部节点不为空,则尝试自旋成功添加到它的后面
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
10. acquireQueued(final Node node, int arg) 方法
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//获取node节点前节点,注意这里如果获取前节点为空时,会报空指针异常
//为啥会为空?节点只有在setHead之后才会将前节点设为Null,我猜测是当前线程被唤醒后,其实它所在的节点就在head位置
final Node p = node.predecessor();
//如果p为头节点(说明node节点就是头节点)并且尝试获取资源上锁成功
//注意这里for是个死循环,说明只有当首节点为当前节点时,才会获取锁资源
//这里可以看出head节点,并非出于阻塞状态,即使它等待状态为小于等于0
//所以说 出于阻塞队列的节点并非每一个节点都处于阻塞状态除头部节点以外
if (p == head && tryAcquire(arg)) {
//node取代了头节点
setHead(node);
//这里的p实质为头节点,头节点没有prev节点,所以没有对象引用它,
//但是它会引用下一个节点,这里将下节点置为Null,意味着该节点没有
//被引用,它也没引用其他节点,有利于gc回收
p.next = null; // help GC
failed = false;
return interrupted;
}
//shouldParkAfterFailedAcquire对前面节点状态进行初始化,并去掉状
//态大于0的节点,如果前节点以初始化了就会通过
//parkAndCheckInterrupt方法挂起,可以通过LockSupport.unpark将其
//唤醒,唤醒后,设置中断标识并清除中断标识,以便后续相应中断
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
//获取锁资源失败,则会分析删除掉当前节点并唤醒后面节点的线程
//第一种情况,啥时候走到这里来呢,我看了下前面的predecessor方法,该方法有一个抛出空指针的异常,
//如果当前的前节点为空,则会抛出一个空指针异常,那啥时候前节点会为空呐?我也不知道
//2.第二种情况就是tryAcquire(arg)方法会抛出一个Error
//啥情况下会出现c + acquires呐?就是当前锁状态为释放状态,但是又释放了一次,说明该节点在同一时间被唤醒了多次,比如该节点在头节点或者在第二节点,都会被唤醒,这里后面我会提到
// int nextc = c + acquires;
//if (nextc < 0)
// throw new Error("Maximum lock count exceeded");
cancelAcquire(node);
}
}
11. shouldParkAfterFailedAcquire(Node pred, Node node) 方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//如果前节点处于SIGNAL等待状态,配合parkAndCheckInterrupt方法一直处于阻塞状态
//SIGNAL 该状态代表 后面的线程可以被唤醒
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
//向前轮询(注意这里跳过了前节点,直接从前前节点开始),知道找到前前节点waitStatus小于等于0为止,这个时候将前节点与node节点关系闭环
//这里相当于跳过了一些等待状态大于0的节点,直接与上一个小于等于0的节点形成前后节点关
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//如果前节点等待状态小于等于0则通过自旋将前节点等待状态改为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
12. parkAndCheckInterrupt() 方法
private final boolean parkAndCheckInterrupt() {
//挂起当前线程
LockSupport.park(this);
//返回当前线程中断标志位,并清除中断标志位
return Thread.interrupted();
}
关于中断的demo
public static void main(String[] args) {
new Thread(() -> {
Thread.currentThread().interrupt();
System.out.println("设置中断标志后");
System.out.println("Thread.currentThread().isInterrupted() => " + Thread.currentThread().isInterrupted());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("该线程处于睡眠,相应中断,中断可以为sleep,wait,park,join等等");
e.printStackTrace();
}
System.out.println("设置清除中断标志后 " + Thread.interrupted());
System.out.println("Thread.currentThread().isInterrupted() => " + Thread.currentThread().isInterrupted());
}
关于方法parkAndCheckInterrupt demo
public static void main(String[] args) {
Test test = new Test();
Thread thread = new Thread(() -> {
System.out.println("running!");
if (test.parkAndCheckInterrupt()){
System.out.println("中断");
}
});
thread.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
LockSupport.unpark(thread);
System.out.println("running end!");
}
private final boolean parkAndCheckInterrupt() {
System.out.println(Thread.currentThread().getName() + "阻塞该线程!");
LockSupport.park(Thread.currentThread());
System.out.println(Thread.currentThread().getName() + "该线程恢复");
return Thread.interrupted();
}
13.park(Object blocker) 方法
public static void park(Object blocker) {
Thread t = Thread.currentThread();
//通过UNSAFE.putObject(t, parkBlockerOffset, arg);方法,直接给当前线程的成员变量parkBlocker赋值为AbstractQueuedSynchronizer实例
setBlocker(t, blocker);
//对当前线程进行阻塞
UNSAFE.park(false, 0L);
//清除当前线程parkBlocker
setBlocker(t, null);
}
14. cancelAcquire(Node node)
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
// Skip cancelled predecessors
Node pred = node.prev;
//找到一个前节点状态小于等于0为止,这里跳过了前前节点
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
Node predNext = pred.next;
node.waitStatus = Node.CANCELLED;
//由于node.prev = pred = pred.prev,所这里的前节点为前前节点(后面前前节点后面统称为前节点)
//如果当前节点为尾部节点,则通过自旋将尾部节点设置为前节点,并将前节点的后节点设置为null
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
int ws;
//如果前节点为SIGNAL状态并且前节点不是头节点,如果前节点状态小于0则通过自旋将其设置为SIGNAL状态,注意这里只要条件成立前节点状态就是SIGNAL状态也就是初始化等待状态,且前节点线程不为空,就是位于中间节点
//以上条件成立,如果node节点下一节点状态小于等于0,则会将Node节点删除,将前节点和后节点通过自旋将两个进行关联
//这里总结下,就是当前节点位于中间,前节点满足小于等于0状态就改为初始状态,后面节点满足小于等于0状态,就将前后节点进行衔接
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
//主要作用如果node节点后存在状态小于等于0的节点,则将该节点作为node后节点,并唤醒该节点对应的线程
//这里有以下几个场景才会去触发
//1.前节点为头节点
//2.前节点为非初始化状态,比如状态大于0
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
总结:该方法主要是清理该节点在队列的引用,并清理该节点前面几个状态大于0的节点,如果后节点为等待状态,如果条件满足就唤醒后面节点的线程
15. unparkSuccessor(Node node)方法
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
//如果当前节点状态小于0则将其状态通过自选改为0,小于0指该线程处于等待状态,现在将其重新改为0状态,代表线程已被唤醒
//这里为啥会将头节点等待状态设置为0? 因为头部节点释放了锁资源(上面
//acquireQueued方法说过,在获取到锁时,会将当前线程设置为头节点,
//所以说头部节点对应的线程不是处于等待状态的,
//所以这里也很好解释了为啥会唤醒头部节点后面的等待线程,而不是唤醒head节点线程)
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
//如果node下一个节点为空,或者状态大于0则将其置为空
//我们知道小于等于0是等待状态,那么这里就会跳过不是等待的线程,直接唤醒下一个最近处于等待的线程,并头节点下节点指针指向该节点
if (s == null || s.waitStatus > 0) {
s = null;
//从尾部向前轮询,直到轮询到Node节点为止(注意这里没有break,也就是说找到左边第一个状态小于0的,然后放置node节点后出,然后唤醒该节点的线程),如果找到一个节点状态小于等于0,则将该节点赋值到Node后节点中
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//这里说明node后节点存在状态小于等于0的节点,这个时候通过调用LockSupport.unpark唤醒这个节点的线程
if (s != null)
LockSupport.unpark(s.thread);
}
16. unlock() 方法
public void unlock() {
//锁状态减一
sync.release(1);
}
17. release(int arg) 方法
public final boolean release(int arg) {
//将当前所资源减一,如果锁被释放掉了(状态为0)
if (tryRelease(arg)) {
Node h = head;
//如果队列头部节点不为空,并且等待状态不为0,则会唤醒后面的队列线程
//也就是说从头部开始唤醒后面的节点
if (h != null && h.waitStatus != 0)
//唤醒h后节点的线程争夺锁资源
unparkSuccessor(h);
return true;
}
return false;
}
18. tryRelease(int releases) 方法
protected final boolean tryRelease(int releases) {
//当前锁状态减去需要减去的状态
int c = getState() - releases;
//如果上下文为当前线程,且锁状态为0时才会释放所资源
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
总结:
获取锁几个步骤:
- 尝试通过自旋获取对应所资源,获取成功将上下文设置为当前线程,并且锁状态设置为1
- 如果通过自旋没有获取到锁,则会再次尝试获取锁,还获取不到,则会将该线程当做一个节点放到一个队列当中的结尾处,此时发现队列当中
- 如果该节点为头部节点,此时会不停地轮询尝试获取锁,直到获取到锁为止,
如果发现该节点不是头部节点,则会将该节点前节点没有初始化就将其初始化为SIGNAL状态,并且会清理掉前面节点状态大于0的节点,此时如果前节点已经初始化好了,就会将当前线程挂起,知道前面线程释放所资源,将其唤醒。
释放锁几个步骤
- 如果锁状态通过减一变为0时,则会将当前锁资源释放,上下文改为null,锁状态重置为0
- 如果发现队列头部不为空且状态不为0,则会唤醒第一个后面节点状态小于等于0的线程,并且这个节点前面的节点都会被删除
释放锁唤醒demo仅供参考
public static void main(String[] args) {
Test test = new Test();
AtomicBoolean flag = new AtomicBoolean(true);
Thread thread = new Thread(() -> {
try {
for (; ; ) {
Thread.sleep(1000);
System.out.println("running");
if (nextHashCode.compareAndSet(0,1)){
System.out.println("获取资源成功");
}
if (flag.get() && test.parkAndCheckInterrupt()){
System.out.println("继续获取所资源");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("end");
}
});
thread.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread thread1 = new Thread(() -> {
System.out.println("释放锁锁状态设置为0");
flag.set(false);
nextHashCode.set(0);
System.out.println("唤醒头节点线程抢夺资源");
LockSupport.unpark(thread);
});
System.out.println("5s已到 锁资源释放");
thread1.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程被中断");
thread.interrupt();
}
private final boolean parkAndCheckInterrupt() {
System.out.println(Thread.currentThread().getName() + "阻塞该线程!");
LockSupport.park(Thread.currentThread());
System.out.println(Thread.currentThread().getName() + "该线程恢复");
return Thread.interrupted();
}
咱们回过头来看看公平锁
看看公平锁的 tryAcquire 和 lock方法 和非公锁的有啥区别
//非公平锁
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
//公平锁
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
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;
}
}
公平锁与非公平锁在方法上的区别
1.lock 方法
非公平锁多了一个步骤,会去尝试通过自旋去获取锁,而公平锁则不会
2. tryAcquire 方法
非公平锁直接调用父类sync方法nonfairTryAcquire(int acquires)方法,而公平锁则自己写了一套定制逻辑,对比父类nonfairTryAcquire(int acquires)方法,主要体现在锁释放时,多了一个判断,就是hasQueuedPredecessors()这个方法下面咱们来看看这个方法代码
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
// 头节点不等于尾节点 证明不是单节点
//第二个节点为空,或者不是当前线程
//结合公平锁tryAcquire 方法来看,只有取反的成立的条件下才会去获取获取锁资源
//只有该条件不成立才会去自旋获取锁
//什么情况下不成立
//1. 头节点等于尾节点或者说队列没有节点也就是说被没有初始化 也就是说队列要么只有一个单节点在等待要么队列没有节点,这个时候锁资源为释放状态,这个时候去获取锁资源是合理的
//2. 队列至少含有两个节点,而且第二个节点为当前线程,为啥第二个节点可以去竞争锁资源呐?这个时候咱们看看enq(final Node node) 方法,如果尾节点tail为空,则会new出来一个空壳node节点去填充这个队列,由于第一个节点是空壳子,那么在这种情况下,第二个线程是有权利去竞争锁资源的
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
总结:
公平锁与非公平锁的区别,公平锁每次去获取锁资源时,都会去判断队列有没有排队的线程,如果没有才会去获取锁资源,因为队列是先进先出的阻塞队列, 所以说每个线程在竞争锁资源时是公平的的。
而非公平锁,在线程可每次获取锁资源时,都会去尝试下能不能获取到锁资源,而且即使到所资源释放时,这个时候去获取锁资源也会去尝试获取锁资源,相比于公平锁多了两种条件去获取锁资源,没有判断队列存在的排队线程。
这里我可以举一个例子,公平锁去地铁乘车,如果前面没有人排队,或者只有一个举着排队警示牌的地铁工作人员在那里排队,这个时候公平锁就会刷地铁卡进去,否则有人在那排队的话,他就会乖乖的站在队列的末尾。
如果非公平锁去地铁乘车,发现前面有人在排队,他会去尝试挤进去刷地铁卡,这个时候就看排队列第一的乘客和他哪个先刷卡成功了,如果刷卡不成功的话,但是恰好第一的乘客刷成功进去了,此时他则会再次尝试刷卡进去,如果还是刷卡不成功才会乖乖的排在队列的末尾去默默等待前面那个人刷完再去刷。
Condition篇
这里我就不介绍Condition的用途了
通过下面的例子咱们就了解了
现在有一个场景,就是有十个小伙伴,每个小伙伴轮流上来咬一口,当每个人都咬了一口后,再重复上面的场景,注意这里小伙伴通过线程来实现
看我写的demo
package com.skindow.sql;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* @ Author :syc.
* @ Date :Created in 16:11 2021/3/10
* @ Description:
* @ Modified By:
* @ Version:
*/
public class ConditionLockTest {
public static ReentrantLock lock = new ReentrantLock();
public static final Integer i = 10;
public static final AtomicBoolean[] flag = new AtomicBoolean[i];
public static final Condition condition = lock.newCondition();
static {
for (int i1 = 0; i1 < flag.length; i1++) {
flag[i1] = new AtomicBoolean(true);
}
}
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor executorService = new ThreadPoolExecutor(i, i + 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
for (int j = 0; j < i; j++) {
executorService.submit(new myThread(j));
}
Thread.sleep(10000);
}
public static class myThread extends Thread {
private Integer i;
myThread(Integer i) {
this.i = i;
}
@Override
public void run() {
try {
lock.lock();
while (flag[i].get()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 获取到锁");
System.out.println(Thread.currentThread().getName() + " 释放锁进入等待!");
if (lock.getQueueLength() == 0){
System.out.println("唤醒所有等待的线程");
condition.signalAll();
}
condition.await();
System.out.println(Thread.currentThread().getName() + " 等待状态解除!开始获取锁!");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
打印如下
pool-1-thread-2 获取到锁
pool-1-thread-2 释放锁进入等待!
pool-1-thread-3 获取到锁
pool-1-thread-3 释放锁进入等待!
pool-1-thread-1 获取到锁
pool-1-thread-1 释放锁进入等待!
pool-1-thread-4 获取到锁
pool-1-thread-4 释放锁进入等待!
pool-1-thread-5 获取到锁
pool-1-thread-5 释放锁进入等待!
pool-1-thread-6 获取到锁
pool-1-thread-6 释放锁进入等待!
pool-1-thread-7 获取到锁
pool-1-thread-7 释放锁进入等待!
pool-1-thread-8 获取到锁
pool-1-thread-8 释放锁进入等待!
pool-1-thread-9 获取到锁
pool-1-thread-9 释放锁进入等待!
pool-1-thread-10 获取到锁
pool-1-thread-10 释放锁进入等待!
唤醒所有等待的线程
pool-1-thread-2 等待状态解除!开始获取锁!
pool-1-thread-2 获取到锁
pool-1-thread-2 释放锁进入等待!
pool-1-thread-3 等待状态解除!开始获取锁!
pool-1-thread-3 获取到锁
pool-1-thread-3 释放锁进入等待!
pool-1-thread-1 等待状态解除!开始获取锁!
pool-1-thread-1 获取到锁
pool-1-thread-1 释放锁进入等待!
pool-1-thread-4 等待状态解除!开始获取锁!
pool-1-thread-4 获取到锁
pool-1-thread-4 释放锁进入等待!
pool-1-thread-5 等待状态解除!开始获取锁!
pool-1-thread-5 获取到锁
pool-1-thread-5 释放锁进入等待!
pool-1-thread-6 等待状态解除!开始获取锁!
pool-1-thread-6 获取到锁
pool-1-thread-6 释放锁进入等待!
pool-1-thread-7 等待状态解除!开始获取锁!
pool-1-thread-7 获取到锁
pool-1-thread-7 释放锁进入等待!
pool-1-thread-8 等待状态解除!开始获取锁!
pool-1-thread-8 获取到锁
pool-1-thread-8 释放锁进入等待!
pool-1-thread-9 等待状态解除!开始获取锁!
pool-1-thread-9 获取到锁
pool-1-thread-9 释放锁进入等待!
唤醒所有等待的线程
- await 方法相当于wait,意思让当前线程挂起,并且释放当前锁
- signalAll 方法一定要作用于锁的上下文环境,否则会报错,该方法是唤醒所有当前通过当前condition导致等待的线程
在这里插入代码片
Condition 源码分析
通过下面的结构图可以看到await方法是AbstractQueuedSynchronizer阻塞队列中的内部类ConditionObject 实现Condition接口操作队列来实现的,跟队列相关
1. await方法
public final void await() throws InterruptedException {
//如果当前线程含有中断标志,则会抛出一个中断异常并清除中断标志,
//这里可以和Object中wait方法机制一致,处于等待状态都会抛出中断异常
if (Thread.interrupted())
throw new InterruptedException();
//添加一个新的节点到队列的尾部(注意这里会清除不是CONDITION状态的节点)
Node node = addConditionWaiter();
//释放当前node锁资源,并返回释放锁资源前锁的状态码
int savedState = fullyRelease(node);
int interruptMode = 0;
//判断该线程在队列中是否处于挂起的状态,不处于挂起状态则执行waile里面的条件语句
//这里配合signalAll 方法,因为在调用signalAll方法后,就会将唤醒的等待节点放置阻塞队列中,一旦放到阻塞队列中就会跳出该循环,达到唤醒的目的
while (!isOnSyncQueue(node)) {
//挂起当前的线程
LockSupport.park(this);
//检查等待中断
//如果线程被中断,则判断尝试添加一个节点在末尾,成功返回-1,否则返回1并清除中断标识
//要跳出这个while循环,
//1. isOnSyncQueue返回true,就是该节点处于阻塞队列中了,意味着该节点已经处在等待状态了
//2. 该线程被中断过
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//acquireQueued 从队列中尝试node节点去获取锁资源,并且上锁标识为
//savedState,也就是await还原之前的锁状态,interruptMode !=
//THROW_IE 也就是说上面自旋失败,为啥失败,因为node节点等待状态不为
//CONDITION状态(也就是说该节点已被唤醒),所以interruptMode !=
//THROW_IE ,以为这该节点不为CONDITION状态,
//注意acquireQueued返回值,只要线程中断才会返回true
//也就是说下面这个if条件成立,那么必然该线程以获取到锁(注意
//acquireQueued方法,该方法是个阻塞方法,虽然
//checkInterruptWhileWaiting会清除中断,但是在阻塞时依然会被中断),并
//且该线程被中断过,且状态标识不是CONDITION状态
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//如果它后面还存在等待节点,则清除队列中不为CONDITION状态的等待节点
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
//interruptMode 我个人理解就是说,在await方法被中断才会为
//THROW_IE,如果在获取锁的时候中断会被重置为REINTERRUPT
if (interruptMode != 0)
//最后,如果await期间被中断,则抛出中断异常
//如果获取所期间被中断,则重新设置当前线程的终端标识
reportInterruptAfterWait(interruptMode);
}
2. addConditionWaiter() 方法
private Node addConditionWaiter() {
//获取末尾等待节点
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
//如果尾等待结点不为空且不为CONDITION状态,被该状态表示的节点,通过该信号量来表示该节点处于等待状态对应await方法
if (t != null && t.waitStatus != Node.CONDITION) {
//去掉不为CONDITION状态的等待节点
unlinkCancelledWaiters();
//注意这里执行了unlinkCancelledWaiters方法后,t则为最后一个CONDITION状态的节点
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION);
//t为空,要么队列为空,要么队列不存在CONDITION状态的节点(这里不存在这种情况,因为执行了unlinkCancelledWaiters方法),当不存在CONDITION状态的尾节点,就添加一个以当前线程状态为CONDITION的节点当做头节点,否则就将该节点作为最后一个CONDITION状态节点的后节点
//新添加的节点作为尾等待节点并返回最新的等待节点(尾节点)
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
3. unlinkCancelledWaiters() 方法
总结:
轮询ConditionObject对象所有节点,去除掉其中不为CONDITION状态的节点
private void unlinkCancelledWaiters() {
//获取第一个等待节点
Node t = firstWaiter;
Node trail = null;
//从第一节点向后轮询
while (t != null) {
//next为下一个等待节点
Node next = t.nextWaiter;
//t是作为轮询前的节点,当t状态为CONDITION,则trail保存该节点的值,trail是作为中间变量的存在,它的意义是保留轮询前CONDITION状态的节点,而且trail有一个特征,那就是trail是队列最后一个CONDITION状态的节点
//如果trail为空,证明轮询前节点状态不是CONDITION或者第一次进来两种情况,由于条件有限,这里先考虑第一次进来,这个时候t就是头节点,但是头节点不是CONDITION状态,这个时候将头节点的下节点取代它,后面eles条件就是第一次轮询完后才有可能成立的情况,当trail不为空,证明轮询前节点为CONDITION状态,如果当前t不是CONDITION状态,就将后节点作为前节点(注意这里是前一个CONDITION状态的节点)。
//这里注意下t.nextWaiter = null;清理不为CONDITION状态的节点
//如果next为空,将前一个CONDITION状态的节点作为标记为尾节点
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}
4. fullyRelease(Node node) 方法
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
//释放锁资源,释放成功就返回原先的锁资源状态码,否则就抛出一个异常
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
//抛出异常后,并将当前node节点等待状态标记为CANCELLED,意味着当前线程释放锁失败,将其等待节点状态改为CANCELLED,这里配合unlinkCancelledWaiters就不难理解了。
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
5. isOnSyncQueue(Node node) 方法
总结:返回true标识这该线程已经处于等待状态了,否则该线程处于准备挂起的状态
final boolean isOnSyncQueue(Node node) {
//判断当前节点是否处于等待状态,或者前面节点为空
//说白了就是判断当前node是否处于等待且排队的状态
//注意node节点是从conditionObject拿出来的,不是node对象拿出来的
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
//到这里说明node不是CONDITION状态 且该节点前存在节点 (注意这里是节点,不是等待节点)await时添加等待节点
//这时存在后节点就返回true
//说明它后面还存在排队的情况
if (node.next != null) // If has successor, it must be on queue
return true;
//到这里说明node不是CONDITION状态 且该节点前存在节点 且不存在后节点
//如果在前节点找到它对应的Node节点,这名该节点已经处于排队等待状态了所以直接返回ture
//这里结合await方法,返回ture情况,findNodeFromTail返回ture,证明它无需再挂起该Node线程,这里不难理解node.next != null返回ture,这里标识该node节点已经存在队列中了。
return findNodeFromTail(node);
}
6. findNodeFromTail(Node node) 方法
private boolean findNodeFromTail(Node node) {
//根据上面的条件,到这里node是不存在后节点的,所以这里向前轮询,直到找到对应的节点为止
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
7. checkInterruptWhileWaiting(Node node) 方法
private int checkInterruptWhileWaiting(Node node) {
//如果该线程被中断,则添加在队列添加一个新节点状态为0,添加成功返回-1否则返回1
//如果未被中断则返回0
//清除中断标识
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
8. transferAfterCancelledWait(Node node) 方法
final boolean transferAfterCancelledWait(Node node) {
//在阻塞队列中添加一个新的节点,状态为0(注意这里用的是自旋,也就是当前节点处于准备挂起的状态CONDITION才会去在队列末尾添加一个新节点)
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
enq(node);
return true;
}
9.reportInterruptAfterWait(int interruptMode) 方法
//REINTERRUPT 设置当前线程的中断标志
//THROW_IE 直接抛出中断异常
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
if (interruptMode == THROW_IE)
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}
10. signalAll() 方法
public final void signalAll() {
//判断获取到锁的线程是否为当前线程,不是的话就会抛出IllegalMonitorStateException异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignalAll(first);
}
11. doSignalAll(Node first) 方法
private void doSignalAll(Node first) {
//清除节点的头等待节点和尾等待节点
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
//将等待节点添加到一个阻塞队列中,队列中新的节点状态为SIGNAL
transferForSignal(first);
first = next;
} while (first != null);
}
12. transferForSignal(Node node) 方法
final boolean transferForSignal(Node node) {
//将node节点状态从CONDITION改为0,不为CONDITION状态的不用考虑
//注意,这里为啥将等待节点的状态该为0,第一 说明该节点准备唤醒,第二 配合后面unlinkCancelledWaiters方法,它会删除非CONDITION状态的节点
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//走到这里证明该状态成功,在队列尾部添加一个节点,并返回前节点
//这里为啥要在Node对象中添加一个节点?
//因为你想啊,我要把他唤醒,但是他也得重新排队啊,
//不可能不排队直接唤醒,从这也说明,signalAll方法是从第一个往后面一一唤醒的,
//不会打破先入先出的规则
Node p = enq(node);
int ws = p.waitStatus;
//如果前节点状态大于0,意味着前节点被取消,可以尝试将其唤醒
//如果发现前节点状态小于等于0,则将其改为SIGNAL状态,也就是排队等待状态,状态修改失败,则尝试将其唤醒
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
Condition 挂起和总结
具体细节就不多说了,简单写下过程
await挂起
- 如果发现线程被中断,则挂起会抛出中断异常
- 在ConditonObject中,该对象含有一个头等待节点和尾等待节点,将当前线程当做一个节点存放在尾节点后方
- 释放当前锁资源
- (注意这里为死循环)如果当前节点处于Conditon状态(这个状态表示当前线程是由于Conditon接口挂起的)或者阻塞队列中不存在该线程节点(为啥这么判断?是为后面Signal唤醒做铺垫,一旦调用Signal跳出这个死循环),则将当前线程挂起。到这一步,整个挂起流程就完毕了,后面就等待唤醒了。从第五步开始就是节点被唤醒后的流程了
- 触发该线程对应的节点去尝试获取锁资源
- 判断当前节点后是否存在等待节点,如果有就清除被取消的节点
- 最后如果在await期间被中断,则会抛出中断异常,如果在获取锁期间被中断,则会重新设置中断标志
Signal 和 signalAll 清除等待状态
- 将ConditonObject 中的等待节点从第一个节点开始,一一存放到阻塞队列中
- 如果发现前节点被取消,或者前节点状态小于等于0尝试将状态改为signal状态失败,则会将前节点唤醒
- 唤醒后则会从上面第四部开始
注意
Signal 和 signalAll并非唤醒相关线程,只是更改其状态,也就是准备状态,并不会马上被唤醒,唤醒的触发机制除了上面的第二步,还有当其他线程(这里的其他不包含被await的线程)释放完锁之后,才会去触发其中队列的第二个节点的唤醒,如果第二节点为他们其中的一个,这个时候才会去唤醒其中的一个
说白了就是 Signal只是通知那些被等待的线程,你们现在可以去排队了争夺cpu执行权了,不用一直等下去了。至于什么时候能获取到cpu执行权,就看队列中前方是否还有其他线程在排队了
最后在总结下节点状态的说明
- CANCELLED 说明由之前的状态变为CANCELLED状态,代表该节点之前的状态被取消了,比如在Condition中,如果调用了Condition的await接口,该接口会添加一个condition状态的节点在ConditionObject对象中,标识由Condition发起的等待,但是这仅仅是个标志,还没有处于等待中,在调用wait方法之后,需要释放当前的锁资源,由于释放锁失败,所以这里Condition状态改为了CANCELLED状态,意味着该节点被取消等待了
- SIGNAL 被SIGNAL标识的节点,可以unpark它后面的节点线程
- CONDITION 由Condition接口发起的特殊等待标识,意味着被标识的线程被Condition挂起