拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java多线程基础笔记:生产者-消费者模型

Java多线程基础笔记:生产者-消费者模型

前言

本文面向编程零基础小白,用生活化案例通俗讲解 Java 中多线程核心概念、组成要素与完整实操流程,手把手演示生产者-消费者模型的完整可运行代码示例。

一、核心概念

线程
线程是程序里的一条“执行流”。 一个进程可以有多条线程,它们共享进程的内存,但各自可以有独立的栈和程序计数器。

在 Java 中,有多种不同的方式可以创建独立线程,比如通过继承 Thread 类并重写 run 方法,在需要启动线程时将写好的线程功能类封装成一个对象,并调用 start 方法。但 Java 是单继承,这样会占掉继承名额。

还有一种方式是实现 Runnable 接口,并重写 run 方法。但 Runnable 接口并没有线程的功能,它只是提供了需要重写的 run 方法,这个接口中并没有 start 方法,只有 run 方法,直接调用 run 方法虽然可以跑起来,但这并非一个独立线程,它会在调用它的那条线程里执行。如果是在 main 方法里调用,就会卡住主线程,在 run 方法执行完之前,主线程什么都做不了。

真正要作为独立线程使用,必须在重写完 run 方法后,新建一个 Thread 类的对象,以及一个实现了 Runnable 接口的类的对象,并将这个对象作为参数传给 Thread 对象,才可以通过调用 start 方法来新建独立线程的对象。
这次代码中使用的是实现 Runnable 接口的方式。

生产者
生产者线程在模型中负责执行:
每隔一段时间生产一定的数据,放入库中,直到库存达到一定数量或没有多余空间后,让线程进入等待状态,直到库存减少至一定量,或是库有了空位后恢复运行。

消费者
消费者线程在模型中负责执行:
每隔一段时间消费库中的一定的数据,直到库存减少到一定数量或没有库存后,让线程进入等待状态,直到库存增加至一定量后恢复运行。

生产与消费
在生产者-消费者模型中,并非简单的“先生产一个、再消费一个”之类的先后顺序,而是同时生产与消费。如果只有一条主线程,的确只能实现有先后顺序的生产与消费,要实现同时生产与消费则需要用到独立线程。

基于 Java 封装的特性,我们将写好的生产者和消费者类,通过先前封装独立线程对象的方法,把它们的对象作为任务参数传给 Thread 对象,让每一个 Thread 对象成为一个独立线程,它们可以同时进行,在一个进程中同时执行生产与消费的功能。

“仓库”类
主函数、生产方法、消费方法,以及用来存放产出数据的“仓库”都在该类中,生产者与消费者类的 run 方法中只有生产与消费的“耗时”与调用方法的句子。

这么做是有原因的,因为我们虽然需要生产和消费能够同时进行,但如果在同一瞬间只能进行一个操作,比如补一次货,或者消费一次,否则,假如库存差一个就满仓时,两个生产者同时调用方法,A 先检查,发现没满,还没等 A 写入,B 也检查了一遍,也发现没满。然后 A 写入,size 增加;B 紧接着也写入,size 再加一次。数据被覆盖,且 size 超出数组上限,下一轮生产就会直接数组越界。
另一种情况就是仓库中只剩下一份数据,但同时有两个消费者线程来进行消费行为,就会导致有一个消费者拿不到有效数据。

所以,生产和消费的方法都在库里写,且用 synchronized 关键字修饰,就可以避免那些异常情况。
synchronized 关键字的功能是,被其修饰的方法在同一时间内,只允许所有线程的其中一个线程调用,谁抢到谁就可以先用。但如果单独写在线程类中,就算是用 synchronized 修饰过的方法也可能会出现异常,因为单独写在线程类中,就意味着 synchronized 锁的对象,从“所有线程共享的那个对象”变成了“每个线程自己的对象”。这样做与不写 synchronized 没有区别。

代表仓库的数组也用 private 修饰,并写在了仓库类中,也是因为所有线程操控的是同一个仓库,如果单独写在线程类中,就变成了“你生产你的,我消费我的”。

二、生产、消费模型的组成要素

  1. 生产方法
    方法中的第一句就是先判断库存是否已满,如果满了就会令调用该方法的线程进入等待状态。如果没有满,就生产一份数据放进仓库,并输出行为和库存情况。最后调用 notifyAll() 方法,令所有处于等待状态的线程继续运行,这样消费者就可以继续消费这些新产出的数据。
publicsynchronizedvoidproduce(intdata){while(list.length==size){//仓库已满,进入等待try{wait();}catch(InterruptedExceptione){thrownewRuntimeException(e);}}list[size]=data;//生产数据size++;System.out.println(Thread.currentThread().getName()+" 生产了 "+data+",库中数据量:"+size);notifyAll();//唤醒正在等待的线程}
  1. 消费方法
    消费方法中第一句也是先判断库存情况,如果没有库存了,就会令调用这个方法的线程进入等待。如果还有库存,就会消费一个数据,使仓库的库存减少一份,然后输出行为和库存情况。最后也会调用 notifyAll() 方法,令所有正在等待的生产者可以继续生产。
publicsynchronizedintconsume(){while(0==size){//仓库为空,进入等待try{wait();}catch(InterruptedExceptione){thrownewRuntimeException(e);}}size--;intdata=list[size];//消费数据list[size]=0;System.out.println(Thread.currentThread().getName()+" 消费了 "+data+",库中数据量:"+size);notifyAll();//唤醒正在等待的线程returndata;}
  1. 主函数
    启动程序后,会先新建一个仓库类的对象,并创建3个生产者线程,和3个消费者线程,然后把仓库的对象通过构造方法传给每一个线程,这样所有的线程操控的都会是同一个仓库,而不是各干各的。
publicstaticvoidmain(String[]args){Bufferbuffer=newBuffer();//只建一个仓库,所有线程共享for(inti=0;i<3;i++){Producerproducer=newProducer(buffer);//把同一个仓库传给每个生产者Threadt=newThread(producer,"生产者"+i);t.start();}for(inti=0;i<3;i++){Consumerconsumer=newConsumer(buffer);//把同一个仓库传给每个消费者Threadt=newThread(consumer,"消费者"+i);t.start();}}}

三、完整实操案例:

生产者线程类:

importstaticjava.lang.Thread.sleep;publicclassProducerimplementsRunnable{privateBufferbuffer;//生产者要操作的共享仓库publicProducer(Bufferbuffer){this.buffer=buffer;}@Overridepublicvoidrun(){while(true){buffer.produce(1);//调用仓库的生产方法try{sleep(1000);//每隔1秒生产一次}catch(InterruptedExceptione){thrownewRuntimeException(e);}}}}

消费者线程类:

publicclassConsumerimplementsRunnable{privateBufferbuffer;//消费者要操作的共享仓库publicConsumer(Bufferbuffer){this.buffer=buffer;}@Overridepublicvoidrun(){while(true){buffer.consume();//调用仓库的消费方法try{Thread.sleep(2000);//每隔2秒消费一次}catch(InterruptedExceptione){thrownewRuntimeException(e);}}}}

“仓库”类:

publicclassBuffer{publicint[]list=newint[20];//仓库,最多存20个数据publicintsize=0;//当前库存publicsynchronizedvoidproduce(intdata){while(list.length==size){//仓库已满,进入等待try{wait();}catch(InterruptedExceptione){thrownewRuntimeException(e);}}list[size]=data;//生产数据size++;System.out.println(Thread.currentThread().getName()+" 生产了 "+data+",库中数据量:"+size);notifyAll();//唤醒正在等待的线程}publicsynchronizedintconsume(){while(0==size){//仓库为空,进入等待try{wait();}catch(InterruptedExceptione){thrownewRuntimeException(e);}}size--;intdata=list[size];//消费数据list[size]=0;System.out.println(Thread.currentThread().getName()+" 消费了 "+data+",库中数据量:"+size);notifyAll();//唤醒正在等待的线程returndata;}publicstaticvoidmain(String[]args){Bufferbuffer=newBuffer();//只建一个仓库,所有线程共享for(inti=0;i<3;i++){Producerproducer=newProducer(buffer);//把同一个仓库传给每个生产者Threadt=newThread(producer,"生产者"+i);t.start();}for(inti=0;i<3;i++){Consumerconsumer=newConsumer(buffer);//把同一个仓库传给每个消费者Threadt=newThread(consumer,"消费者"+i);t.start();}}}

四、个人收获总结

写这次练习时,一开始我认为这东西没看着还挺简单,没想太多就试着把生产和消费的功能代码直接写在线程类的 run 方法中,结果就是前面写到过的,在同一瞬间有一个以上的消费者或生产者操作仓库导致数组相关的异常。后来我查了些资料,了解到似乎可以用 synchronized 关键字来避免这种情况。

于是我就把功能代码拿出来专门写一个方法,并且用 synchronized 来修饰这个方法,但我还是在线程类中写的方法。但结果前面也写到了,还是会出现异常。后来才把它们写到仓库类中,随着构造方法传进仓库对象给内部对象赋值后,通过对象调用方法,才解决了所有的问题。
这次练习,加深了我对独立线程对象相关内容的理解。

返回列表