前面讲到当多个线程共享一个变量的时候,为保证线程安全必须通过加锁的方式实现线程之间的互斥,那么问题来了,当某个线程执行完毕之后满足了另外一个线程的执行条件时如何通知另外一个线程执行呢?听起来似乎比较难理解,举一个生产者和消费者的例子,一个面包箱内能盛放20个面包,有一个生产者负责生产面包然后放到这个容器中,有一个消费者从面包箱内取出然后吃掉面包,逻辑上的执行应该是这样的,首先生产者生产一箱面包然后通知消费者吃掉面包,当消费者吃光这一箱面包之后通知生产者再生产一箱面包,然后消费者再次吃光面包,循环执行。映射到代码上的执行逻辑应该是这样的,首先由两个线程一个生产者生产面包的线程,一个是消费者消费面包的线程,初始时两个线程分别判断当前是否存在面包,如果不存在则消费者线程阻塞,生产者线程执行生产面包的任务,当生产者生产的面包数目达到20个(即已生产满一箱)后再次判断发现箱子已经装满,此时本本线程阻塞,然后通知消费者线程执行,消费者线程经过判断发现箱子中已经有面包了,所以启动消费线程直到箱子中的面包被吃光,然后阻塞本线程通知生产者线程启动生产任务。那么如何通过代码实现这个过程呢?有两种方式,一种是通过传统方法两个线程持有同一个对象,调用该对象的wait和notify方法来阻塞/通知线程,另外一种就是通过Lock对象,获取该对象的Condition对象,通过condition对象完成不同线程之间的通信,也是本文主要讲解的方式。下面先通过代码对比一下两种,首先来看传统方法代码:
public class Test {
static boolean flag = false;
private static int count = 0;
public static void main(String[] args){
final ProductorAndConsumer p = new ProductorAndConsumer();
new Thread(new Runnable(){
@Override
public void run() {
while(true){
p.product();
}
}
}).start();
new Thread(new Runnable(){
@Override
public void run() {
while(true){
p.consume();
}
}
}).start();
}
static class ProductorAndConsumer{
public synchronized void product(){
while(flag){//当flag为true的时候阻塞生产
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
for(int i=1;i<=20;i++){
count++;
System.out.println("已生产"+count+"个面包");
if(20 == count){
flag = true;
this.notify();
}
}
}
public synchronized void consume(){
while(!flag){
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
for(int i=1;i<=20;i++){
count--;
System.out.println("还剩"+count+"个面包");
if(0 == count){
flag = false;
this.notify();
}
}
}
}
}
使用wait和notify能规避死锁问题,但并不能完全避免,必须在编程过程中避免死锁。在使用过程中需要注意的几点是:首先,wait、notify方法是针对对象的,调用任意对象的wait()方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任意对象的notify()方法则将随机解除该对象阻塞的线程,但它需要重新获取改对象的锁,直到获取成功才能往下执行;其次,wait、notify方法必须在synchronized块或方法中被调用,并且要保证同步块或方法的锁对象与调用wait、notify方法的对象是同一个,如此一来在调用wait之前当前线程就已经成功获取某对象的锁,执行wait阻塞后当前线程就将之前获取的对象锁释放。当然假如你不按照上面规定约束编写,程序一样能通过编译,但运行时将抛出IllegalMonitorStateException异常,必须在编写时保证用法正确;最后,notify是随机唤醒一条阻塞中的线程并让之获取对象锁,进而往下执行,而notifyAll则是唤醒阻塞中的所有线程,让他们去竞争该对象锁,获取到锁的那条线程才能往下执行。那么如果有多个线程,而且各个线程之间有严格的执行顺序时使用notify方法随机唤醒某个线程或notifyAll方法唤醒所有等待线程让他们去竞争执行的方法就不可以了,此时使用Condition方式唤醒指定的某个线程可以完美解决这个问题,代码如下:
public class ProductorAndConsumer {
static Lock lock = new ReentrantLock();
static Condition proCondition = lock.newCondition();
static Condition conCondition = lock.newCondition();
final static List<Bread> bread = new ArrayList<Bread>();
static int count=0;
/**
* @param args
*/
public static void main(String[] args) {
final Productor productor = new Productor();
final Consumer consumer = new Consumer();
new Thread(
new Runnable() {
@Override
public void run() {
while(true){
Bread x = new Bread();
productor.product(x);
}
}
}
).start();
new Thread(
new Runnable() {
@Override
public void run() {
while(true){
consumer.consume();
}
}
}
).start();
}
static class Productor {
public void product(Bread x){
lock.lock();
try{
while(bread.size()>=10){//定义列表容积不能超过10个
try {
proCondition.await();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
bread.add(x);
count++;
System.out.println("生产者生产了一个面包,当前面包数量为:" + count);
conCondition.signal();
}finally{
lock.unlock();
}
}
}
static class Consumer {
public void consume(){
lock.lock();
try{
while(count <= 0){
try {
conCondition.await();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
bread.remove(count-1);
count--;
System.out.println("消费者消费了一个面包,当前面包数量为:" + count);
proCondition.signal();
}finally{
lock.unlock();
}
}
}
}
class Bread {
}
Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition1的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,在阻塞队列那一篇博文中就讲述到了,阻塞队列实际上是使用了Condition来模拟线程间协作。 Condition是个接口,基本的方法就是await()和signal()方法,依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition() ,需要注意的是调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用 ,除了以上两种方法之外,实现线程之间通信的方式还有LockSupport,它提供的park和unpark方法分别用于阻塞和唤醒,而且它提供避免死锁和竞态条件,有需要的话可以深入研究一下。