java.util.concurrent 包在java语言中可以说是比较难啃的一块,但理解好这个包下的知识,对学习java来说,不可谓是一种大的提升,我也尝试着用自己不聪明的脑袋努力的慢慢啃下点东西来。其实 java.util.concurrent 包中,最核心的就是AQS( AbstractQueuedSynchronizer) 这个抽象类,可以说是整个JUC包的基石,但今天先不说AQS,我先从比较容易理解的 Condition 条件讲起。
什么是Condition条件
Condition 是定义在 java.util.concurrent.locks 包下的一个接口,这个接口主要的功能就是实现了与Object类中的wait(),notify()方法相同的语义,但是功能上更强大。Condition 接口中定义的方式其实很少,列举下来:
//使当前线程接到signal信号之前,或者被中断之前一直处于等待状态
void await() throws InterruptedException;
//使当前线程接到signal信号之前一直处于等待状态
void awaitUninterruptibly();
//使当前线程接到signal信号之前,或到达指定等待的时间之前,或者被中断之前一直处于等待状态
boolean await(long time, TimeUnit unit) throws InterruptedException;
//使当前线程接到signal信号之前,或到达指定的最后期限时间之前,或者被中断之前一直处于等待状态
boolean awaitUntil(Date deadline) throws InterruptedException;
//使当前线程接到signal信号之前,或到达指定等待的时间之前,或者被中断之前一直处于等待状态
long awaitNanos(long nanosTimeout) throws InterruptedException;
//向一个线程发送唤醒信号
void signal();
//向所有线程发送唤醒信号
void signalAll();
从定义的方法中也可以看出这个类的功能,无非就两种:等待方法、唤醒等待方法。
Condition 的使用
下面用一个之前用的小demo展示一下 Condition 的使用方法:
先使用Object中的wait,notify 方法:
public class TestCondition {
public static void main(String[] args) {
try {
ThreadTest t1 = new ThreadTest("t1");
synchronized (t1) {
System.out.println(Thread.currentThread().getName()+"线程启动线程 t1");
t1.start();
System.out.println(Thread.currentThread().getName()+"线程执行wait方法,等待被唤醒");
t1.wait();
System.out.println(Thread.currentThread().getName()+"线程继续执行");
}
} catch (InterruptedException e)
e.printStackTrace();
}
}
}
class ThreadTest extends Thread{
public ThreadTest(String name){
super(name);
}
public void run(){
synchronized (this) {
System.out.println(Thread.currentThread().getName()+"线程执行 notify 方法");
notify();
}
}
}
运行结果:
main线程启动线程 t1
main线程执行wait方法,等待被唤醒
t1线程执行 notify 方法
main线程继续执行
这里就不再详述关于 synchronized 关键字的使用,举这个示例是为了引出Condition的使用。
稍微改一下上面的程序:
public class TestCondition {
private static ReentrantLock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
public static void main(String[] args) {
try {
ThreadTest t1 = new ThreadTest("t1");
lock.lock();
System.out.println(Thread.currentThread().getName()+"线程启动线程 t1");
t1.start();
System.out.println(Thread.currentThread().getName()+"线程执行condition.await()方法,等待被唤醒");
condition.await();
System.out.println(Thread.currentThread().getName()+"线程继续执行");
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
lock.unlock();
}
}
static class ThreadTest extends Thread{
public ThreadTest(String name){
super(name);
}
public void run(){
try{
lock.lock();
System.out.println(Thread.currentThread().getName()+"线程执行condition.signal()方法");
condition.signal();
}finally{
lock.unlock();
}
}
}
}
运行结果:
main线程启动线程 t1
main线程执行condition.await()方法,等待被唤醒
t1线程执行condition.signal()方法
main线程继续执行
根据上面的实例可以看出,Condition工具类是配合Lock类一起使用的,当然Condition的作用远不止上面代码这样简单,其实它最主要的作用是可以在同一把锁上,针对不同的业务使用不用的Condition。比如在前面的文章提到的生产消费问题,我们完全可以使用两个Condition,一个针对于生产,一个针对于消费,当产品为0时,我们可以让消费的Condition执行await方法,当产品不为0时,可以让消费的Condition执行signal方法。生产者也是类似,具体代码这里就不再详述了,可以尝试自己实现一下。
Condition 的实现类:ConditionObject
Condition 是一个接口,那它到底是怎么工作的呢?我们来看一下ReentrantLock 类是怎么样使用Condition 的。
Condition condition = lock.newCondition();//这是生成Condition 的方法
追踪一下newCondition( )这个方法,我们就可以看到一个Condition 的具体实现:
public Condition newCondition() {
return sync.newCondition(); //调用sync的newCondition
}
//sync的方法,返回一个new ConditionObject()
final ConditionObject newCondition() {
return new ConditionObject();
}
这个sync是什么呢?它是定义在ReentrantLock 中的内部类,它继承了上面提到的AQS这个抽象类,从某种角度来说ReentrantLock 只是提供了一个可以操作AQS这个核心类的入口,代理了一些重要的方法,那为什么不让ReentrantLock 直接继承AQS,而是选择用一个内部类来实现呢,可能是出于一些安全性方面的考虑。ConditionObject是定义在AQS中的。
Condition 与 AQS 到底是怎么工作的?
探究这个问题,就需要来看一下ConditionObject的源码了。
代表性的,我们看一下await方法和signal方法的源码(基于jdk1.8.0_112):
await方法:
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException(); //判断线程的中断状态,如果中断则抛出异常
//将当前线程包装成一个条件等待节点添加到ConditionObject维护的一个队列中
Node node = addConditionWaiter();
//释放当前线程占有的锁
int savedState = fullyRelease(node);
int interruptMode = 0;
//这个while 循环就是在当前线程释放锁后,一直观察持有自己线程的节点有没有被加载到
//AQS维护的等待队列中(加入到这个队列中才有获取锁的资格),什么时候会加入到这个队列中呢?
//当然是执行了唤醒这个线程对应的singal方法的时候啦
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
在看一下signal方法:
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;//ConditionObject维护队列中的头节点,进行唤醒操作
if (first != null)
doSignal(first);
}
从上面的叙述不难看出,AQS 和 ConditionObject各自维护了一个队列,用来存放包装了线程的Node节点。联系上面的代码示例和源码可以总结一下AQS和ConditionObject中的队列是怎么被维护的:
(1) 示例中存在两个线程:main线程 t1线程
(2) 当main线程调用lock.locak()方法时,main获取到锁,继续执行, 当执行到t1.start()方法时,t1也想获取lock,但是此时该锁已经被main线程占用,所以t1线程进入 AQS维护的等待队列中,等待机会获取cpu的占用权。
(3) 当main线程执行了condition.await()方法时,main线程就在释放占用锁的同时加入到了ConditionObject维护的等待队列中,在这个队列中的线程,如果signal状态不发生改变,是永远没有机会获取到cpu的占有权的。
(4) 好,这个时候main已经进入了ConditionObject维护的等待队列中,那么AQS维护的等待队列中的t1线程就可以获取cpu的占有权了,可以继续执行。
(5) 当t1线程执行到 condition.signal() 方法时,就会唤醒ConditionObject维护的等待队列中的头节点,也就是main线程。但是注意,这里唤醒的意思是将main线程节点放到AQS维护的等待队列中,然后听从AQS的调度,并不是马上就能获取cpu的占有权。
(6) 然后t1线程执行结束,unlock释放占用的锁,在AQS维护的等待队列中的main就能继续执行下去了。
总之:
- ConditionObject维护的等待队列的功能是存放那些执行了await方法的线程,等待收到signal信息好可以进入AQS维护的等待队列中。在这个队列中是不会获取到锁的。
- AQS维护的等待队列存放那些就绪状态的线程,只等待目前占有锁的家伙执行完或者进入了ConditionObject维护的等待队列中之后,来进行竞争获取锁。只有在这个队列中才有资格(并不一定会)获取锁。