前言:自己尝试着用java多线程实现了操作系统原理中讲到的“生产者-消费者”模型,在这里和大家分享一下遇到的问题和心得。我们姑且模糊“线程”和“进程”的区别,只记住它们都是可并发执行的一组过程即可。
一、什么是“生产者-消费者”模型?
这个模型所描述的是假设有一个能容纳N个产品的工厂,生产者进程不断向工厂中输入产品,而消费者进程不断从工厂中取走产品。尽管所有的生产者和消费者都是已异步的方式运行的,但它们之间必须保持同步,即不允许消费者进程到一个空的工厂中取产品,也不允许生产者进程向一个已经装满产品的工厂投放产品。
二、用java多线程实现“生产者-消费者”模型(详细代码)
- package cn.uestc.fz;
- public class ProducerConsumer {
- public static void main(String[] args) {
- Factory factory = new Factory(0);
- ProducerThread pt1 = new ProducerThread(factory, 10);
- ProducerThread pt2 = new ProducerThread(factory, 10);
- ProducerThread pt3 = new ProducerThread(factory, 10);
- ConsumerThread ct1 = new ConsumerThread(factory, 10);
- ConsumerThread ct2 = new ConsumerThread(factory, 10);
- ConsumerThread ct3 = new ConsumerThread(factory, 10);
- pt1.setName("生产者1号");
- pt2.setName("生产者2号");
- pt3.setName("生产者3号");
- ct1.setName("消费者1号");
- ct2.setName("消费者1号");
- ct3.setName("消费者1号");
- pt1.start();
- pt2.start();
- pt3.start();
- ct1.start();
- ct2.start();
- ct3.start();
- }
- }
- /*工厂类*/
- class Factory{
- private static final int MAX_NUMBER=100;
- private int currentNumber;
- public Factory(int currentNumber) {
- super();
- this.currentNumber = currentNumber;
- }
- public void add(int produceNumber){
- //同步方法,保证向工厂添加产品操作是互斥的
- synchronized(this){
- while(currentNumber+produceNumber>MAX_NUMBER){
- System.out.println(Thread.currentThread().getName()+":当前生产过剩!无法投放!");
- try {
- wait();
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- currentNumber+=produceNumber;
- System.out.println(Thread.currentThread().getName()+":投放了"+produceNumber+"个产品,当前工厂里产品的数量为:"+currentNumber);
- notifyAll();
- }
- try {
- Thread.sleep(100);//休眠0.1秒,好让其它线程有机会运行
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- public void get(int consumeNmber){
- //同步方法,保证从工厂消费产品操作是互斥的
- synchronized(this){
- while(currentNumber<consumeNmber){
- System.out.println(Thread.currentThread().getName()+":工厂剩余产品不足!无法消费!");
- try {
- wait();
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- currentNumber-=consumeNmber;
- System.out.println(Thread.currentThread().getName()+":消费了"+consumeNmber+"个产品,当前工厂里产品的数量为:"+currentNumber);
- notifyAll();
- }
- try {
- Thread.sleep(100);//休眠0.1秒,好让其它线程有机会运行
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- }
- /*生产者线程*/
- class ProducerThread extends Thread{
- private Factory factory;
- private int produceNumber; //一次投放的产品个数
- public ProducerThread(Factory factory,int produceNumber){
- this.factory=factory;
- this.produceNumber=produceNumber;
- }
- public void run(){
- while(true)
- factory.add(produceNumber);
- }
- }
- /*消费者线程*/
- class ConsumerThread extends Thread{
- private Factory factory;
- private int consumerNumber; //一次取出的产品个数
- public ConsumerThread(Factory factory, int consumerNumber) {
- this.factory = factory;
- this.consumerNumber = consumerNumber;
- }
- public void run(){
- while(true)
- factory.get(consumerNumber);
- }
- }
运行结果:
生产者1号:投放了10个产品,当前工厂里产品的数量为:10
生产者2号:投放了10个产品,当前工厂里产品的数量为:20
消费者1号:消费了10个产品,当前工厂里产品的数量为:10
消费者1号:消费了10个产品,当前工厂里产品的数量为:0
消费者1号:工厂剩余产品不足!无法消费!
生产者3号:投放了10个产品,当前工厂里产品的数量为:10
消费者1号:消费了10个产品,当前工厂里产品的数量为:0
生产者2号:投放了10个产品,当前工厂里产品的数量为:10
生产者1号:投放了10个产品,当前工厂里产品的数量为:20
消费者1号:消费了10个产品,当前工厂里产品的数量为:10
消费者1号:消费了10个产品,当前工厂里产品的数量为:0
生产者3号:投放了10个产品,当前工厂里产品的数量为:10
消费者1号:消费了10个产品,当前工厂里产品的数量为:0
生产者2号:投放了10个产品,当前工厂里产品的数量为:10
生产者1号:投放了10个产品,当前工厂里产品的数量为:20
消费者1号:消费了10个产品,当前工厂里产品的数量为:10
生产者3号:投放了10个产品,当前工厂里产品的数量为:20
消费者1号:消费了10个产品,当前工厂里产品的数量为:10
消费者1号:消费了10个产品,当前工厂里产品的数量为:0
生产者1号:投放了10个产品,当前工厂里产品的数量为:10
生产者2号:投放了10个产品,当前工厂里产品的数量为:20
消费者1号:消费了10个产品,当前工厂里产品的数量为:10
消费者1号:消费了10个产品,当前工厂里产品的数量为:0
生产者3号:投放了10个产品,当前工厂里产品的数量为:10
消费者1号:消费了10个产品,当前工厂里产品的数量为:0
生产者1号:投放了10个产品,当前工厂里产品的数量为:10
生产者2号:投放了10个产品,当前工厂里产品的数量为:20
消费者1号:消费了10个产品,当前工厂里产品的数量为:10
生产者3号:投放了10个产品,当前工厂里产品的数量为:20
消费者1号:消费了10个产品,当前工厂里产品的数量为:10
消费者1号:消费了10个产品,当前工厂里产品的数量为:0
生产者2号:投放了10个产品,当前工厂里产品的数量为:10
......
理想状态下此程序应该会源源不断的运行着。
三、关于操作系统中的进程(or线程)转换的三种状态
现代操作系统一般采用分时操作系统,也就是说进程(or线程)在计算机上运行都是分时间片的,计算机的时间被划分为一个个细小的时间片,分给需要运行的进程(or线程)用。这里必须要清楚的是,在单CPU的计算机上,同一时间只能有一个进程(or线程)在运行,这就是我们说的“并发”,而并不是“并行”。一旦某个进程(or线程)用完时间片之后,它会退回到“就绪”状态,而如果运行的中途出现问题,如缺少资源而导致无法运行的时候,它就会变成阻塞的状态。这个时候操作系统的调度程序会根据某种调度算法来将就绪队列中的一个进程(or线程)提升为运行状态。而那个被阻塞的进程(or线程)在得到它所缺少的资源后会变为就绪状态,等待调度程序把它调度执行。
四、为什么要用Thread类的静态方法sleep让当前的进程休眠?
我们知道,其实各个进程(or线程)就是切换着运行的,但是为了让我们观察的更明显,每次完成一次取出或者投放操作,都让当前的线程休眠掉,这样子其它线程就有就会执行。当当前的线程执行sleep操作后,相当于把它给阻塞掉了(此时这个线程的时间片也许并没有用完),sleep的参数是毫秒。也就是说这个毫秒的时间过后,操作系统会给此线程一个信号(相当于缺少的资源)说你的时间到了,然后把它变为就绪状态等待新一轮的调度。(注意就算sleep后的时间到了,该线程可能也不会马上运行,只能说它有了运行的机会)
五、为什么Factory类中add和get方法要加synchronized同步块?
这里就要介绍操作系统中“临界区”的概念了,“临界资源”指的是各进程互斥的共享的资源,也就是我有的时候你不能有,你有的时候我不能有的东西,但是当我没有了,你就可以有了。而“临界区”就是对“临界资源”访问的那段代码,显然我们要求一次只能有一个进程或者线程访问这段代码。在java中每个对象都有唯一的一把“锁”,当某个线程得到这把锁的时候,其它线程只能在外面徘徊,直到这个线程放弃这把锁,其它等待锁的线程再去竞争这把锁。
那么如果我们不使用synchronized同步块会怎么样呢?如果当前工厂中有0个产品,这时有一个生产者线程pt1执行了“currentNumber+=produceNumber”的操作,碰巧这个时候线程发生切换,另一个生产者线程pt2也执行了“currentNumber+=produceNumber”的操作,然后线程再次切换回pt1,此时打印出来的是“生产者1号:投放了10个产品,当前工厂里产品的数量为:20”,其实正确的应该结果是“10”。这显然是不对的。
而当加了synchronized同步块后,线程pt1拥有factory对象的锁,此时pt2是无法运行这段代码的,它只能等待pt1释放了这把锁再去执行。
六、wait方法和notifyAll方法是怎么用的?
这几个方法都是存在于Object类中的。
wait
public final void wait() throws InterruptedException
-
在其他线程调用此对象的
notify()
方法或notifyAll()
方法前,导致当前线程等待。换句话说,此方法的行为就好像它仅执行 wait(0) 调用一样。当前线程必须拥有此对象监视器。该线程发布对此监视器的所有权并等待,直到其他线程通过调用
notify
方法,或notifyAll
方法通知在此对象的监视器上等待的线程醒来。然后该线程将等到重新获得对监视器的所有权后才能继续执行。对于某一个参数的版本,实现中断和虚假唤醒是可能的,而且此方法应始终在循环中使用:
synchronized (obj) { while (<condition does not hold>) obj.wait(); ... // Perform action appropriate to condition }
此方法只应由作为此对象监视器的所有者的线程来调用。有关线程能够成为监视器所有者的方法的描述,请参阅notify
方法。
notify
public final void notify()
-
唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个
wait
方法,在对象的监视器上等待。直到当前线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。
此方法只应由作为此对象监视器的所有者的线程来调用。通过以下三种方法之一,线程可以成为此对象监视器的所有者:
- 通过执行此对象的同步实例方法。
- 通过执行在此对象上进行同步的
synchronized
语句的正文。 - 对于
Class
类型的对象,可以通过执行该类的同步静态方法。
一次只能有一个线程拥有对象的监视器。
notifyAll
public final void notifyAll()
-
唤醒在此对象监视器上等待的所有线程。线程通过调用其中一个
wait
方法,在对象的监视器上等待。直到当前线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。
此方法只应由作为此对象监视器的所有者的线程来调用。有关线程能够成为监视器所有者的方法的描述,请参阅
notify
方法。
首先我们必须明白,以上几个方法必须在同步方法或者同步代码块中调用,否则会抛出java.lang.IllegalMonitorStateException的异常。也就是说,在线程执行以上几个方法的时候,它必须拥有该对象的锁(一次只能有一个线程拥有对象的监视器)。
当在对象上调用wait方法时,执行该代码的线程立即放弃它在对象上面的锁。然后当调用notify时,并不意味着调用notify的线程会立即放弃其锁。如果此线程仍然在完成同步代码,则线程在执行完这段代码后才会放弃其锁。因此,调用notify方法时并不意味着这时该锁变得可用。我们可以看到API上面的解释是:直到当前线程放弃此对象上的锁定,才能继续执行被唤醒的线程。参考:http://blog.csdn.net/iceman1952/article/details/2159812
七、notify和notifyAll的区别
notify就是在所有wait的线程中选择一个,使它被唤醒;
notifyAll就是使所有wait的线程被唤醒。
那么问题就来了,当notify所在的那段同步代码执行完后,释放掉该对象的锁,那么notify唤醒的那个线程就一定会得到该锁吗?我在网上看到很多都是这样说的,我认为这是错误的。我们应该明确,“唤醒”和“得到锁”不是一个概念,“唤醒”只是有机会得到锁罢了,虽然notify只是唤醒一个线程,但是别忘了,可能还有其它的线程要进入synchronized同步块,它们也有可能获得这把锁。API中也说了:被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。
简单点说:notify使一个wait的线程唤醒,它有机会得到锁,其它wait的线程连得到锁的机会都没;notifyAll使所有wait的线程唤醒,它们都有机会得到锁。
八、为什么条件判断要用while而不用if
我先开始写的时候用的是if,觉得你如果不满足条件,就去等待嘛,然后等着被唤醒然后得到锁。但是别忘了,就算你得到了锁,只是代表你可以运行,并不意味着条件就满足了。比如说,生产者线程1发现工厂满了,等待之,生产者线程2也发现满了,等待,消费者1消费了之后执行notifyALL操作,生产者1和2都被唤醒,1得到了锁执行,它也调用notifyAll,这时2得到了锁,但是它能往工厂添加产品吗?此时工厂又满了啊,所以每次被唤醒,都要再次进行条件判断。
这个可能有点绕,简单点说,就是唤醒你的线程并不一定是和你业务逻辑相关的线程,上面的生产者2就是被生产者1唤醒的,而我们想象中可能认为它是被某个消费者线程唤醒的。
好了,先写这么多,有什么再补充吧!