一、Condition
在并发情况下进行线程间的协调,如果是使用的 synchronized
锁,我们可以使用 wait/notify
进行唤醒,如果是使用的 Lock
锁的方式,则可以使用 Condition
进行针对性的阻塞和唤醒,相较于 wait/notify
使用起来更灵活。那 Condition
是如何实现线程的等待和唤醒的呢,本篇文章带领大家一起解读下 Condition
的源码。
在进行源码分析前,先回顾下 Condition
是如何使用的,例如下面一个案例:
public class Test {
public synchronized static void main(String[] args) throws InterruptedException {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
lock.lock();
System.out.println("线程1开始等待!");
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1被唤醒继续执行结束!");
lock.unlock();
}, "1").start();
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
System.out.println("开始唤醒线程!");
condition.signal();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程2执行结束!");
lock.unlock();
}, "2").start();
}
}
运行之后,可以看到下面日志:
由于在第一个线程中,使用的 condition.await()
因此当前线程会被阻塞挂起,而第二个线程,在 1s
后进行了 condition.signal()
操作,因此第一个线程会被唤醒继续执行。这里细心的小伙伴应该可以发现,第一个线程阻塞时锁并没有释放,而第二个线程在1s
后也成功拿到锁了,所以表明在 condition.await()
时会自动释放当前锁,这点和 wait
相同,在第二个线程进行了 condition.signal()
操作,第一个线程并没有继续向下执行,而是等待第二个线程处理完才会继续执行,由此可以表明被唤醒的线程会重新获取锁,成功获取锁后继续执行。
下面通过源码看下 Condition
是如何实现的等待唤醒。
二、Condition 源码解读
2.1. lock.newCondition() 获取 Condition 对象
首先看下在使用 lock.newCondition()
获取一个Condition
对象时,具体做了什么,这里以 ReentrantLock
为例,进入到 ReentrantLock
的 newCondition()
方法中,又执行了 Sync
的 newCondition()
方法,再进去就会发现其实是 new
了一个 ConditionObject
类对象:
下面点到这个类中,可以看到其实是 AbstractQueuedSynchronizer
下的一个子类:
2.2. condition.await() 阻塞过程
了解到 Condition
的对象后,下面就可以看下 condition.await()
方法了,点到该类下的 await()
方法中:
其中 addConditionWaiter()
则是将自己加入到链表中,并获取到当前线程所在的 Node
,这里注意下 Node
的状态是 Node.CONDITION
也就是 -2
,后面会依赖于该状态。
下面再回到 await()
方法继续向下看,接着使用了 fullyRelease
方法传入了当前的 Node
,这里的 fullyRelease
方法主要做了释放当前线程锁的操作。
点到 release
方法中,主要执行了 unparkSuccessor
,如果看过 Lock
锁的解锁源码,就会知道其实 unparkSuccessor
就是解锁的过程
下面继续回到 await()
方法中,当释放锁后,进入到了一个 while
循环中,通过查看 isOnSyncQueue
方法,可以看到是可以符合while
的条件也就可以进入到循环中:
在循环中可以明显的看到 LockSupport.park(this)
,将当前线程进行了阻塞。
2.3. condition.signal() 唤醒过程
上面已经看到线程被阻塞了,如果需要被唤醒则需要通过condition.signal()
,这个方法是如何唤醒的呢?
下面来到 AbstractQueuedSynchronizer
类的 signal()
方法中:
主要执行了 doSignal
方法,再点到 doSignal
中,可以看到这里开启了一个循环,对链表的每一个元素都进行了 transferForSignal
操作,这里也比较好理解,就是要唤醒等待中的线程。
下面点到 transferForSignal
中,看下对每个 Node
都做了什么操作。点进去之后也比较好理解,如果状态是 Node.CONDITION
也就是 -2
,刚才在解读 await
方法时就提到这个状态了,这里正好形成了呼应,下面有个非常显眼的操作 LockSupport.unpark(node.thread)
直接唤醒了目标线程。也就是唤醒了 2.2
中的最后一步操作。
2.4. condition.await() 被唤醒后
当 await()
方法中的 LockSupport.park(this)
被唤醒后,继续向下执行,下面会判断下当前线程有没有被打断,如果没被打断则 break
终止循环继续执行。
下面这个 acquireQueued
方法,如果看过 Lock
加锁的源码,应该可以了解到就是上锁的过程
成功获取锁后就会继续执行,被阻塞的线程也就是继续执行。
三、总结
通过上面的源码分析,应该对 Condition
有了新的理解和掌握,细心地小伙伴应该可以发现在源码中好多地方都使用了 CAS
,因此当竞争资源非常激烈时, Lock
的性能要远远优于 synchronized
。