Condition是JDK1.5中提供的一种线程通信方式,用来替代wait和notify,那么肯定有人会问:wait和notify为什么不能用呢?老哥,我用的不错兄弟别着急,我来详细告诉你……Object中推荐使用Condition而不是wait和notify的原因有两个:使用notify会导致线程在极端环境下“假死”;条件具有更高的性能。接下来,我们将通过代码和流程图对以上两种情况进行演示。1、notify线程“假死”所谓线程“假死”就是在使用notify唤醒多个等待线程时,不小心唤醒了一个没有“准备好”的线程,导致整个程序进入阻塞状态状态无法继续执行。以多线程编程中的经典案例生产者和消费者模型为例,我们先来演示一下线程“假死”的问题。1.1普通版在演示线程“假死”问题之前,我们先用wait和notify实现一个简单的生产者和消费者模型。为了让代码更直观,我这里写一个超级简单的实现版本。我们先创建一个工厂类。工厂类包含两个方法,一个是循环生产数据的(store)方法,一个是循环消费数据的(extract)方法。实现代码如下。/***工厂类,消费者和生产者通过调用工厂类实现生产/消费*/classFactory{privateint[]items=newint[1];//数据存储容器(为了演示方便,设置存储容量upto1Element)privateintsize=0;//实际存储大小/***生产方式*/publicsynchronizedvoidput()throwsInterruptedException{//循环生产数据do{while(size==items.length){//注意不能是一个if判断//存储容量满了,block消费完等待消费者唤醒System.out.println(Thread.currentThread().getName()+"EnterBlocking");this.wait();System.out.println(Thread.currentThread().getName()+"被唤醒");}System.out.println(Thread.currentThread().getName()+"开始工作");items[0]=1;//为了演示方便,设置一个固定值size++;System.out.println(Thread.currentThread().getName()+"完成工作");//当生产队列有数据时,通知并唤醒消费者this.notify();}while(true);}/***消费方法*/publicsynchronizedvoidtake()throwsInterruptedException{//循环消费数据do{while(size==0){//Producer没有数据,阻塞等待System.out.println(Thread.currentThread().getName()+"进入阻塞(消费者)");this.wait();System.out.println(Thread.currentThread().getName()+"唤醒(消费者)");}System.out.println("消费工作~");size--;//唤醒生产者添加生产this.notify();}while(true);}}接下来我们创建两个线程,一个是生产者调用put方法,一个是消费者调用take方法。实现代码如下:publicclassNotifyDemo{publicstaticvoidmain(String[]args){//创建工厂类Factoryfactory=newFactory();//ProducerThreadproducer=newThread(()->{try{factory.put();}catch(InterruptedExceptione){e.printStackTrace();}},"producer");producer.start();//ConsumerThreadconsumer=newThread(()->{try{factory.take();}catch(InterruptedExceptione){e.printStackTrace();}},"Consumer");consumer.start();}}执行结果如下:从上面的结果可以看出,生产者和消费者在交替执行任务一个循环,场景很和谐,就是我们想要的正确结果1.2线程“假死”版本当只有一个生产者和一个消费者时,wait和notify方法不会有问题。但是,当生产者增加到两个时,就会出现线程“假死”的问题,**程序的实现代码如下:publicclassNotifyDemo{publicstaticvoidmain(String[]args){//创建工厂方法(工厂类代码不变,这里不再赘述)Factoryfactory=newFactory();//ProducerThreadproducer=newThread(()->{try{factory.put();}catch(InterruptedExceptione){e.printStackTrace();}},"producer");producer.start();//Producer2Threadproducer2=newThread(()->{try{factory.put();}catch(InterruptedExceptione){e.printStackTrace();}},"producer2");producer2.start();//消费者Threadconsumer=newThread(()->{try{factory.take();}catch(InterruptedExceptione){e.printStackTrace();}},"consumer");consumer.start();}}程序执行结果如下:从上面的结果可以看出,当我们将生产者的数量增加到2个时,会造成线程“fake”的问题死亡”阻止执行。当producer2被唤醒阻塞后,整个程序无法继续执行。线程“假死”问题分析我们先把上面程序的执行步骤标记一下,得到如下结果:从上图我们可以看到,在执行步骤④时,此时生产者处于工作状态,而producer2和consumer前者处于等待状态。这时候正确的做法应该是唤醒消费者进行消费,等消费者消费完后再唤醒生产者继续工作;但是此时生产者错误的唤醒了生产者2,而生产者2因为队列已经满了,所以没有继续执行的能力,从而导致整个程序的阻塞。流程图如下:正确的执行过程应该是这样的:1.3使用Condition为了解决线程的“假死”问题,我们可以使用Condition来尝试实现。Condition是JUC(java.util.concurrent)包下的一个类,需要使用Lock锁来创建。Condition提供了三个重要的方法:await:对应wait方法;signal:对应notify方法;signalAll:notifyAll方法。Condition的使用类似于wait/notify。也是先获取锁,然后在锁中等待唤醒。Condition的基本用法如下://创建一个Condition对象Locklock=newReentrantLock();Conditioncondition=lock.newCondition();//Locklock.lock();try{//业务方法....//1.进入等待状态condition.await();//2.唤醒操作condition.signal();}catch(InterruptedExceptione){e.printStackTrace();}finally{lock.unlock();}Tips:正确使用Lock。请记住,Lock的lock.lock()方法不能放在try代码中。如果lock方法在try代码块内,可能会因为其他方法抛出异常,在finally代码块中,unlock解锁被解锁的对象,会调用AQS的tryRelease方法(视具体实现类而定),以及抛出IllegalMonitorStateException异常。回到正题回到本文的主题,如果我们使用Condition来实现线程通信,就可以避免程序“假死”的情况,因为Condition可以创建多个等待集。以本文中的生产者和消费者模型为例,我们可以使用两个等待集,一个用于等待和唤醒消费者,一个用于唤醒生产者,这样就不会出现生产者唤醒生产者的情况(生产者只能唤醒消费者,消费者只能唤醒生产者)这样整个过程就不会“假死”。它的执行流程如下图所示:了解了它的基本流程之后,我们来看一下具体的实现代码。基于Condition的工厂实现代码如下:classFactoryByCondition{privateint[]items=newint[1];//数据存储容器(为了演示方便,设置最多存储1个元素的容量)privateintsize=0;//实际存储大小//创建Condition对象privateLocklock=newReentrantLock();//生产者的Condition对象privateConditionproducerCondition=lock.newCondition();//消费者的Condition对象privateConditionconsumerCondition=lock.newCondition();/***生产方式*/publicvoidput()throwsInterruptedException{//循环生产数据do{lock.lock();while(size==items.length){//注意不能是if判断//生产者进入等待System.out。println(Thread.currentThread().getName()+"进入阻塞");producerCondition.await();System.out.println(Thread.currentThread().getName()+"被唤醒");}System.out.println(Thread.currentThread().getName()+"开始工作");items[0]=1;//为了演示方便,设置一个固定值size++;System.out.println(Thread.currentThread().getName()+"工作完成");//唤醒消费者consumerCondition.signal();try{}finally{lock.unlock();}}while(true);}/***消费方法*/publicvoidtake()throwsInterruptedException{//循环消费数据do{lock.lock();while(size==0){//消费者阻塞等待consumerCondition.await();}System.out.println("Consumerwork~");size--;//唤醒producerproducerCondition.signal();try{}finally{lock.unlock();}}while(true);}}两生产者一消费者的实现代码如下:put();}catch(InterruptedExceptione){e.printStackTrace();}},"producer");producer.start();//Producer2Threadproducer2=newThread(()->{try{factory.put();}catch(InterruptedExceptione){e.printStackTrace();}},"Producer2");producer2.start();//ConsumerThreadconsumer=newThread(()->{try{factory.take();}catch(InterruptedExceptione){e.printStackTrace();}},"consumer");consumer.start();}}程序的执行结果如下图所示:从上面的结果可以看出,当使用条件,生产者,消费者,生产者2会一直交替循环执行,执行离子结果符合我们的预期。2.性能问题。上面我们在演示notify会造成线程“假死”问题的时候,肯定有小伙伴认为,如果把notify换成notifyAll,线程就不会“假死”了。这种做法确实可以解决线程“假死”的问题,但同时,新的性能问题也会随之而来。空谈没用,直接上代码展示。下面是使用wait和notifyAll改进后的代码:/***工厂类,消费者和生产者通过调用工厂类实现生产/消费功能*/classFactory{privateint[]items=newint[1];//DataStoragecontainer(为了演示方便,设置容量最多存储1个元素)privateintsize=0;//实际存储大小/***生产方法*@throwsInterruptedException*/publicsynchronizedvoidput()throwsInterruptedException{//循环生产数据do{while(size==items.length){//注意不能通过if判断//存储容量满了,阻塞等待消费者消费后唤醒System.out.println(Thread.currentThread().getName()+"进入阻塞");this.wait();System.out.println(Thread.currentThread().getName()+"被唤醒");}System.out.println(Thread.currentThread().getName()+"startworking");items[0]=1;//为了演示方便,设置一个固定值size++;System.out.println(Thread.currentThread().getName()+"完成工作");//唤醒所有线程this.notifyAll();}while(true);}/***消费方法*@throwsInterruptedException*/publicsynchronizedvoidtake()throwsInterruptedException{//循环消费数据do{while(size==0){//producer没有数据,阻塞waitSystem.out.println(Thread.currentThread().getName()+"进入阻塞(消费者)");this.wait();System.out.println(Thread.currentThread().getName()+"Wokeup(consumer)");}System.out.println("Consumerwork~");size--;//唤醒所有线程this.notifyAll();}while(true);}}StillIt就是两个生产者加一个消费者,实现代码如下:publicstaticvoidmain(String[]args){Factoryfactory=newFactory();//ProducerThreadproducer=newThread(()->{try{factory.put();}catch(InterruptedExceptione){e.printStackTrace();}},"producer");producer.start();//Producer2Threadproducer2=newThread(()->{try{factory.put();}catch(InterruptedExceptione){e.printStackTrace();}},"Producer2");producer2.start();//ConsumerThreadconsumer=newThread(()->{try{factory.take();}catch(InterruptedExceptione){e.printStackTrace();}},"consumer");consumer.start();}执行结果如下图所示:从上面的结果可以看出,当我们调用notifyAll时,并没有引起线程”“假死”会导致所有生产者被唤醒,但由于只有一个任务需要执行,所有被唤醒的生产者中只有一个会l执行正确的工作,而另一个什么都不做Dry,然后进入等待状态,这种行为对于整个程序来说无疑是不必要的,它只会增加线程调度的开销,导致性能下降整个程序对比Condition的await和signal方法,即使有多个生产者,程序也只会唤醒一个有效的生产者工作,如下图:生产者和生产者2会交替被唤醒工作,所以这样执行时并没有额外的开销,相比notifyAll整个程序的性能会提升很多。总结在本文中,我们通过代码和流程图展示了wait方法和notify/notifyAll方法的使用缺陷。主要有两个缺陷。一是在极端环境下使用notify会导致程序“假死”。使用notifyAll会导致性能下降。因此,强烈推荐使用Condition类来实现线程通信。PS:可能有人会问为什么不使用condition的signalAll和notifyAll来进行性能比较呢?而是用signal和notifyAll来比较?我只想说,既然用signal就可以实现这个功能,为什么还要用signalAll呢?这就好比在25度的暖气房里,可以穿短袖外套,为什么还要穿棉袄呢?
