线程的等待与通知,目的就是为了实现线程间的协作,那一般情况下,我们最容易想到的方式是使用循环以及公共变量,比如:
public class LoopThread {
private volatile boolean flag = true;
public void test() {
new Thread(() -> {
while(flag) {
// TODO 一直死循环,等待退出
Thread.yield(); // 释放CPU,和JVM实现方式有关
}
System.out.println("我接到通知,继续执行后续操作。。。");
}).start();
new Thread(() -> {
try {
// TODO 处理任务
System.out.println("开始处理任务。。。");
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
// TODO 处理完,通知线程
System.out.println("任务处理完成,通知线程。。。");
flag = false;
}).start();
}
}
上面的代码,就是使用了循环加公共变量的方式,这种方式一定程度上能够满足需要,但是它不是最好的方式,而且循环对于cpu的占用和释放都会有相对较高的额外开销。
所以,JDK为我们提供了更为便捷的方式:wait()与notify();这两个方法并不是线程的,而是Object对象的,当然使用方式也并不像我们平时调用普通方法一样,它们的使用是有先决条件:当前线程拥有该对象监视器;我们在前面的文章提到过,关键字synchronized做线程同步,会需要拥有某些对象监视器,比如:
public class WaitAndNotify {
/**
* 会抛出{@link java.lang.IllegalMonitorStateException}
*/
public void waitIllegalMonitor() {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 会抛出{@link java.lang.IllegalMonitorStateException}
*/
public void notifyIllegalMonitor() {
this.notify();
}
public synchronized void waitTodo() {
System.out.println("我将要进入WAITING状态");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我被唤醒,并且我得到了内置锁");
}
public synchronized void notifyThread() {
System.out.println("我将要通知任意一个线程可以继续工作");
this.notify();
System.out.println("通知完成,但是我暂时不释放");
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("即将释放锁资源");
}
public synchronized void waitTodoWithTimed() {
System.out.println("我将要进入TIMED_WAITING状态");
try {
this.wait(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我被唤醒或者等待超时,并且我得到了内置锁");
}
}
从上面代码,我们能看到,如果没有拥有对象监视器,那么直接调用对象的wait()或者notify()方法会抛出异常;代码中的waitTodo()和notifyThread()则能比较清楚的知道通知与等待的运作方式,值得注意的第一点是,当前线程即使调用了notify()方法,也不会立即释放它所拥有的共享资源,仅仅只会唤醒任意一个等待队列中等待相同共享资源的线程,被唤醒的线程依然需要竞争获取共享资源;相似的notifyAll()方法则是唤醒所有等待队列中等待相同共享资源的线程;然后被唤醒的线程如果还没有获取到共享资源,那么它会处于BLOCKED状态。从代码中可以知道wait()方法可以被中断,所以根据业务规则,我们需要做好应对处理;当然还有具有时间限制的等待,如果到时间依旧没有被唤醒,那么自动退出等待状态。
接下来我们看一下如果使用Condition实现等待与通知,在JDK1.5中新增了ReentranLock,一种更为灵活的加锁方式,可以创建多个Condition实现多路通知、选择通知;我们看一下Condition最为简单的用法:
public class ConditionWaitAndNotify {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
/**
* 会抛出{@link java.lang.IllegalMonitorStateException}
*/
public void waitIllegalMonitor() {
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 会抛出{@link java.lang.IllegalMonitorStateException}
*/
public void signalIllegalMonitor() {
condition.signal();
}
public void waitTodo() {
lock.lock();
System.out.println("我请求获取到了锁,我将要进入等待");
try {
condition.await();
System.out.println("从等待状态被唤醒,并且获取到了锁资源,现在继续运行");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 释放锁
}
}
public void signalThread() {
lock.lock();
try {
System.out.println("我获取到了锁,准备要唤醒某个处于等待队列中的具有相同对象监视器的线程");
condition.signal();
System.out.println("唤醒了某个线程,但是我不立马释放锁");
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
System.out.println("释放锁资源");
lock.unlock();
}
}
public void waitTodoWithTimed() {
lock.lock();
System.out.println("我请求获取到了锁,我将要进入有时间限制的等待");
try {
condition.await(5, TimeUnit.SECONDS);
System.out.println("从等待状态被唤醒或者等待超时自动唤醒,并且获取到了锁资源,现在继续运行");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 释放锁
}
}
}
从上面代码中能知道,和wait()、notify()的用法比较相似,都需要获得对象监视器之后才能使用;同样Condition也有signalAll()方法,用来唤醒所有等待队列中相同对象监视器上的线程。那么就有一个问题,我现在需要指定唤醒其中一部分线程,不是一个也不是所有,这种情况下,单纯的使用上面几种方法是不可行的;这时候就可以使用Condition为我们提供的特性了:
public class ConditionWaitAndNotifyPart {
private Lock lock = new ReentrantLock();
private Condition conditionProducer = lock.newCondition();
private Condition conditionOdd = lock.newCondition();
private Condition conditionEven = lock.newCondition();
private volatile boolean isWork = true;
private volatile int number = 0;
private volatile boolean hasPrint = true;
/**
* 主生产者
*/
public void produce() {
try {
lock.lock();
while (isWork) {
number++;
// 大于100停止工作
if (number > 100) {
isWork = false;
conditionEven.signalAll();
conditionOdd.signalAll();
break;
}
hasPrint = false;
if (number % 2 == 0) {
conditionEven.signalAll(); // 通知打印偶数的所有线程
} else {
conditionOdd.signalAll(); // 通知打印奇数的所有线程
}
try {
conditionProducer.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
lock.unlock();
}
}
/**
* 打印奇数
*/
public void printOdd() {
try {
lock.lock();
while(isWork) {
if (!hasPrint) {
System.out.println("print odd ==> " + number);
conditionProducer.signal();
hasPrint = true;
}
try {
conditionOdd.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
lock.unlock();
}
}
/**
* 打印偶数
*/
public void printEven() {
try {
lock.lock();
while (isWork) {
if (!hasPrint) {
System.out.println("print even ==> " + number);
conditionProducer.signal();
hasPrint = true;
}
try {
conditionEven.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
lock.unlock();
}
}
}
以上代码,就使用了Condition实现选择通知,我们可以根据情况不同去通知处理不同类型的线程工作,让通知等待变得更加灵活,上述代码,仅仅只能是一个生产线程,可以多个打印线程,如果需要多个生产线程,还需要进一步完善代码;在生产与消费模型中,我们可以去参考BlockingQueue的实现,它也是使用了Condition来实现的阻塞。
如果有不正确的地方,请帮忙指正,谢谢!