单刀直入,今天就是想说一下ConditionObject源码,闲话不多说。ConditionObject这个单词看着可能有点陌生,不过Condition你总熟悉了吧?在ReentrantLock中,一般都是使用condition.await(),condition.signal()来实现锁的等待唤醒机制。
先看一个简单的例子吧,直接看源码可能有点头痛的。
public class WaitTest {
private static Lock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
public static void main(String[] args) {
// 线程一
Thread thread1 = new Thread(() -> {
try {
lock.lock();
//to do sth
System.out.println("线程一开始");
condition.await();
System.out.println("线程一等待后继续运行");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "Thread1");
//线程二
Thread thread2 = new Thread(() -> {
try {
//线程二等待下,让线程一先开始
Thread.sleep(20);
lock.lock();
//获取锁,再等待,线程一已经进入等待队列,只能等线程二
Thread.sleep(200);
//to do sth
System.out.println("线程二开始");
//唤醒了线程一,但是没有释放锁,即使释放锁了,线程一抢占时间片也需要时间,未必有线程二先执行完
condition.signal();
System.out.println("线程二等待后继续运行");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "Thread2");
thread1.start();
thread2.start();
}
}
看一下执行结果:
线程一开始
线程二开始
线程二等待后继续运行
线程一等待后继续运行
我们来简单的看下这个程序,一个主函数,创建了两个异步线程,使用独占锁ReentrantLock,同时使用了ReentrantLock中的condition等待唤醒机制。
首先,是线程一、线程二抢占锁,当然,线程二睡眠了一小会,相当于让出了锁的使用权,只能在同步队列中阻塞了。
接着,线程一等待,进入了等待队列,让出了lock的所有权。
线程二抢占到锁,开始向下执行。
线程二接着调用了condition.signal(),唤醒了等待队列中的线程一,线程一从等待队列进入了同步队列,然并卵,锁还让线程二占着呢!线程一只能继续等待。
当然,哪怕线程二释放了锁的控制权,就是在condition.signal()之后,执行 lock.unlock(),线程二也会比线程一先执行完,毕竟线程一移动到同步队列,抢占锁都需要时间的,而线程二直接就运行over了。
另外需要提一下的就是,调用condition.signal()之后,并不是说唤醒的线程就能执行了!只是将线程放入到了同步队列,只是相当与就绪而已。这个时候,线程还需要获取到锁的控制权,也就是要最终分配到时间片才能执行,这个需要大家注意下,java中的线程没有唤醒之后就立即能执行的哦!
关于condition,我们要弄明白的有2步:
1、释放锁,进入等待队列
2、唤醒锁,移出等待队列,重新将线程移动到同步队列
下面来看一下源码:
/* lock 锁等待方法 */
public final void await() throws InterruptedException {
//线程是中断的,没法等待,抛异常
if (Thread.interrupted())
throw new InterruptedException();
// 新建一个节点,并添加到等待队列中,这个时候是占有lock锁的
Node node = addConditionWaiter();
// 释放当前线程占有的lock
int savedState = fullyRelease(node);
int interruptMode = 0;
//是否在同步队列中
while (!isOnSyncQueue(node)) {
//阻塞当前
LockSupport.park(this);
//checkInterruptWhileWaiting(node)) 不等于0,跳出,这个是唤醒之后的操作。
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 自旋等待获取到同步状态(即获取到lock)
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
上面这段代码是await等待的主要逻辑,下面来说一下lock中的等待唤醒的主要机制。首先,我们要知道,把线程节点移动到等待队列,那么这个时候,这个线程肯定是持有锁的。
我们先来看第一步,代码都有解释,我来大概说下,首先,当前线程是持有锁的,当前线程新建一个node节点,添加到等待队列的尾部,这个等待队列是FIFO的等待队列,加入队列完成后,就释放掉锁,结束对资源的占用。但是这个时候,这个线程并没有直接阻塞,而是继续运行,但是已经释放锁住的资源了哦。 我们可以看到 fullyRelease(node);下面仍然有代码在执行。
接着就是执行了一个while循环,既然是while循环,那肯定会跳出去,总不可能无限循环吧?首先,while条件如果是false的话,那么自然不会再执行了,还有就是这个循环体中有个标志 ‘break’,如果执行了break,自然也会跳出去!
第一次判断,是否在同步队列中(!isOnSyncQueue(node)),肯定不在啊,刚移出来,在啥子哟?然后就进入循环体, 执行LockSupport.park(this); ,然后就卡这了, 这个时候,第一步算是彻底完成~
我们来瞅瞅里面的部分源码。
the first!添加一个新的节点到等待队列。
private Node addConditionWaiter() {
//将lastWaiter赋值给t
Node t = lastWaiter;
// 节点在等待队列上,但是状态又不是Node.CONDITION,那状态就是CANCELLED(1)了,
// 已经取消调度,直接可以清除了
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
//根据当前进程,新建一个节点。
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
这个没啥难度,就是在等待队列上加了一个节点,我相信大伙都看得懂,(*^▽^*)。里面的一个调用我也贴下吧
/* 取消已取消的服务程序节点 */
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = null;
//首节点不为空,进入循环,就是为了干掉所有滴
while (t != null) {
Node next = t.nextWaiter;
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;
}
}
下面就是比较关键的一段代码了,释放锁
final int fullyRelease(Node node) {
//失败标志
boolean failed = true;
try {
// 获取当前线程的state
int savedState = getState();
//释放锁,调用的lock.release
if (release(savedState)) {
// 释放成功,返回state
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
// 释放失败,那就直接让这个线程节点跪了呗。
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
这段代码就是让出了锁的控制权,和lock中释放锁的代码有点类似。首先是获取线程state,然后是release,在执行完release之后,其实,锁的控制权就已经让出去了,后面的代码都是当前线程的一些处理罢了,返回之后,最终也是会执行到LockSupport.park(this);阻塞当前线程。
这样我们基本能知道await的一个流程:建立等待节点---》进入等待队列---》释放锁---》阻塞等待
下面我们来说第2步,signal——唤醒。
要唤醒线程,我们要明确两点:第一,当前线程必须拿到锁,然后才可以唤醒。第二,唤醒不能指定,等待队列是FIFO,只能唤醒最先进入等待队列的那个线程。
好了,我们类瞅一瞅代码。
// 唤醒最早进入等待队列的线程
public final void signal() {
// 是否拥有锁,没有就抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 获取第一个节点
Node first = firstWaiter;
if (first != null)
// 不为空就唤醒
doSignal(first);
}
// 是否独占锁,公平锁的实现方式
protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
这上面的源码没啥绕的,唤醒的操作都在 doSignal(first); 这个操作里,我们进去看下。
// 唤醒等待线程
private void doSignal(Node first) {
do {
// 等待队列首节点干掉
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
}
//成功就加入到同步队列,结束循环,否则就继续找下一个节点
while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
// 重新设置waitStatus,设置失败,说明已经cancel了,会在循环中干掉
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//下面的代码是不是很熟悉?嘿嘿,没错,就是加入到同步队列中去的操作
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
我们来看下doSignal这段代码,首先,拿到首节点firstWaiter,把这个节点从等待队列中移除掉。然后进行transferForSignal操作,如果状态更新成功,就移动到同步队列中去,结束循环,如果这个节点是已经cancel的,那么继续找下一个节点,直到唤醒等待队列中还存活的第一个节点。
那唤醒所有呢?其实很简单上面不是唤醒一个就结束跳出循环么?我不跳出循环了,一直唤醒不就行了?\(^o^)/~,show me~
// 唤醒所有的等待队列
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
//移出等待队列,添加到同步队列
transferForSignal(first);
first = next;
}
//首节点不为空就一直循环
while (first != null);
}
核心代码就这几行,是不是so easy?
总结来说,就是线程持有锁----》拿到等待队列首节点----》移出等待队列----》添加到同步队列
思路并不是很难理解,代码实现的话,我都贴出来了,如果哪里有错误,大家可以在评论中指出哈~
另外,如果直接看源码比较难受的话,大家可以试着debug下我上面的那个测试main方法,主要的逻辑都能走到哦~
No sacrifice,no victory~