在java线程的使用中,仅仅有线程同步是不够的,还需要线程与线程协作(即通信),生产者/消费者问题是一个经典的线程同步以及通信的案例。下面我们通过他来理解线程协作。 该问题描述了两个共享固定大小缓冲区的线程,即所谓的“生产者”和“消费者”在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。要解决该问题,就必须让生产者在缓冲区满时进入等待(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入等待,等到生产者往缓冲区添加数据之后,再唤醒消费者,通常采用线程间通信的方法解决该问题。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入等待,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。下面是JDK5之前传统线程的通信方式。问题代码如下:
package thread_test;import java.util.ArrayList;import java.util.List;/**定义一个工作区类*/public class WorkArea { List<Object> dataBuffer = new ArrayList<Object>();//装数据的共享缓存区 /** * 取数据 */ public synchronized Object getData(){ while(dataBuffer.size() == 0){ try{ System.out.PRintln("消费者线程: "+Thread.currentThread().getName()+" 未检查到数据,进入等待..."); wait(); System.out.println("消费者线程: "+Thread.currentThread().getName()+" 被唤醒并获得锁,继续执行..."); }catch(InterruptedException e){ e.printStackTrace(); } } Object data = dataBuffer.get(0); dataBuffer.clear();//清空缓存区S System.out.println("消费者线程: "+Thread.currentThread().getName()+"拿到数据,释放锁,结束运行"); notify();//唤醒阻塞队列的某线程到就绪队列 return data; } /** * 写入数据 */ public synchronized void putData(Object data){ while(dataBuffer.size() > 0){ try{ System.out.println("生产者线程: "+Thread.currentThread().getName()+" 检查到数据,进入等待..."); wait(); System.out.println("生产者线程: "+Thread.currentThread().getName()+" 被唤醒并获得锁,继续执行..."); }catch(InterruptedException e){ e.printStackTrace(); } } dataBuffer.add(data); System.out.println("生产者线程: "+Thread.currentThread().getName()+"写入数据,释放锁,结束运行"); notify();//唤醒阻塞队列的某个线程到就绪队列 } /** * 生产者线程 */ static class Producer implements Runnable{ private WorkArea workArea; private Object data = new Object(); public Producer(WorkArea workArea) { this.workArea = workArea; } @Override public void run() { workArea.putData(data); } } /** * 消费者线程 */ static class Customer implements Runnable{ private WorkArea workArea; public Customer(WorkArea workArea) { this.workArea = workArea; } @Override public void run() { workArea.getData(); } } public static void main(String[] args){ WorkArea workArea = new WorkArea(); for(int i=1;i<=3;i++){ new Thread(new Customer(workArea)).start(); new Thread(new Producer(workArea)).start(); } }}输出结果:
消费者线程: Thread-0 未检查到数据,进入等待...生产者线程: Thread-1写入数据,释放锁,结束运行消费者线程: Thread-0 被唤醒并获得锁,继续执行...消费者线程: Thread-0拿到数据,释放锁,结束运行消费者线程: Thread-2 未检查到数据,进入等待...消费者线程: Thread-4 未检查到数据,进入等待...生产者线程: Thread-3写入数据,释放锁,结束运行消费者线程: Thread-2 被唤醒并获得锁,继续执行...消费者线程: Thread-2拿到数据,释放锁,结束运行消费者线程: Thread-4 被唤醒并获得锁,继续执行...消费者线程: Thread-4 未检查到数据,进入等待...生产者线程: Thread-5写入数据,释放锁,结束运行消费者线程: Thread-4 被唤醒并获得锁,继续执行...消费者线程: Thread-4拿到数据,释放锁,结束运行Object 类定义了 wait()、notify() 和 notifyAll() 方法。要执行这些方法,必须拥有相关对象的锁。 Wait() 会让调用线程休眠,直到用 Thread.interrupt() 中断它、过了指定的时间、或者另一个线程用 notify() 或 notifyAll() 唤醒它。 当对某个对象调用 notify() 时,如果有任何线程正在通过 wait() 等待该对象,那么就会唤醒其中一个线程T,从对象的等待集中删除线程 T,并重新进行线程调度。然后,该线程以常规方式与其他线程竞争,以获得在该对象上同步的权利(即获得该对象的锁)。当对某个对象调用 notifyAll() 时,会唤醒所有正在等待该对象的线程。 在上面的代码中,调用对象的wait()方法时,会放在while循环中,而不是if循环,这是因为在没有被通知、中断或超时的情况下,线程还可以唤醒一个所谓的虚假唤醒 (spurious wakeup)。虽然这种情况在实践中很少发生,但是应用程序必须通过以下方式防止其发生,即对应该导致该线程被提醒的条件进行测试,如果不满足该条件,则继续等待。也就是把它放在while循环中。还有,wait和notify方法必须工作于synchronized内部,且这两个方法只能由锁对象来调用。因为调用该方法的线程必须拥有此对象监视器
对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。 sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。 在调用sleep()方法的过程中,线程不会释放对象锁。 而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备,在前一个线程释放锁后,准备就绪队列中的线程开始竞争,获取对象锁进入运行状态。 在等待锁定池中的线程,如果不是时间到了,自动唤醒或者被notify唤醒,那么会永远处于等待状态(你可以把上面代码中的两个notify()注释掉,运行代码,就会发现凡是进入等待状态的线程都没有被唤醒继续执行任务,也不会打印出’结束运行’字段)。而synchronized在一个线程释放掉该锁后,处于准备就绪队列中的线程会竞争获得该对象锁,而未获得的线程仍然处于该队列中。
新闻热点
疑难解答