Thinking in Java---執行緒通訊+三種方式實現生產者消費者問題
前面講過執行緒之間的同步問題;同步問題主要是為了保證對共享資源的併發訪問不會出錯,主要的思想是一次只讓一個執行緒去訪問共享資源,我們是通過加鎖的方法實現。但是有時候我們還需要安排幾個執行緒的執行次序,而在系統內部執行緒的排程是透明的,沒有辦法準確的控制執行緒的切換。所以Java提供了一種機制來保證執行緒之間的協調執行,這也就是我們所說的執行緒排程。在下面我們會介紹三種用於執行緒通訊的方式,並且每種方式都會使用生產者消費者問題進行檢驗。
一。使用Object類提供的執行緒通訊機制
Object類提供了wait(),notify(),notifyAll()三個方法進行執行緒通訊。這三個方法都必須要由同步監視器物件來呼叫,具體由分為以下兩種情況:
1)對於使用synchronized修飾的同步方法,因為該類的預設例項(this)就是同步監視器,所以可以在同步方法中直接呼叫這三個方法。
2)對於使用synchronized修飾的同步程式碼塊,同步監視器是synchronized後面括號裡的物件,所以必須使用該物件呼叫這三個方法。
也就是說,這三個方法只能用於synchronized做同步的執行緒通訊。對著三個方法的具體解釋如下:
wait():導致當前執行緒等待,直到其它執行緒呼叫該同步監視器的notify()或notifyAll()方法來喚醒該執行緒。該wait()方法還可以傳入一個時間引數,這時候等到指定時間後就會自動甦醒。呼叫wait()方法的當前執行緒會釋放對該同步監視器的鎖定。
notify():喚醒在此同步監視器上等待的單個執行緒。如果當前有多個執行緒在等待,則隨機選擇一個。注意只有當前執行緒放棄對該同步監視器的鎖定以後(使用了wait()方法),才可以執行被喚醒的執行緒。
notifyAll():喚醒在此同步監視器上等待的所有執行緒。同樣只要在當前執行緒放棄對同步監視器的鎖定之後,才可以執行被喚醒的執行緒。
使用這種通訊機制模擬的生產者消費者問題如下:
package lkl1;
///生產者消費者中對應的緩衝區
//生產者可以向緩衝區中加入資料,消費者可以消耗掉緩衝區中的資料
//注意到緩衝區是限定了大小的,所以使用迴圈佇列的思想進行模擬
public class Buffer {
//根據迴圈佇列的思想,如果out==in,則表示當前緩衝區為空,不可以進行消費
//如果(in+1)%n==out,則表示當前緩衝區為滿,不可以進行生產(這樣會浪費一個空間)
private int n; ///緩衝區大小
private int num; //當前元素個數
//定義一個大小為n的緩衝區
private int buffer[];
//表示當前可以放置資料的位置,初始為0
private int in=0;
//表示當前可以讀取資料的位置,初始為0
private int out=0;
Buffer(int n){
this.n=n;
buffer=new int[n];
num=0;
}
//下面是生產和消費的方法
//生產操作,向緩衝區中加入一個元素x
public synchronized void product(int x){
try{
if((in+1)%n==out){
wait(); //如果緩衝區已滿,則阻塞當前執行緒
}
else{
buffer[in]=x;
in=(in+1)%n;
System.out.println(Thread.currentThread().getName()+"生產一個元素: "+x);
num++;
System.out.println("當前元素個數為: "+num);
notifyAll(); //喚醒等待當前同步資源監視器的執行緒
}
}
catch(InterruptedException ex){
ex.printStackTrace();
}
}
///消費操作,一次取出一個元素
public synchronized void comsumer(){
try{
if(in==out){ //如果緩衝區為空,阻塞當前執行緒
wait();
}
else{
int xx=buffer[out];
out=(out+1)%n;
num--;
System.out.println(Thread.currentThread().getName()+"消費了一個元素: "+xx);
System.out.println("當前元素個數為: "+num);
notifyAll();
}
}
catch(InterruptedException ex){
ex.printStackTrace();
}
}
}
package lkl1;
import java.util.Random;
//生產者執行緒
//會不斷的往緩衝區中加入元素
public class Product extends Thread{
//當前執行緒操作的緩衝區物件
private Buffer buffer;
private Random rand;
Product(){}
Product(Buffer buffer){
this.buffer=buffer;
rand=new Random();
}
public void run(){
while(true){
//向緩衝區中新增一個隨機數
buffer.product(rand.nextInt(100));
}
}
}
package lkl1;
//生產者執行緒
public class Consumer extends Thread{
private Buffer buffer;
Consumer(){}
Consumer(Buffer buffer){
this.buffer=buffer;
}
public void run(){
while(true){ //每次都消耗掉緩衝區中的一個元素
buffer.comsumer();
}
}
}
package lkl1;
//測試
public class BufferTest {
public static void main(String[] args){
Buffer buffer = new Buffer(10);
//一個生產者,多個消費者
new Product(buffer).start();
new Consumer(buffer).start();
new Consumer(buffer).start();
}
}
二。使用Condition控制執行緒通訊
前面我們講同步方式的時候,除了synchronized關鍵字,還講了可以使用Lock進行顯示的加鎖。在使用Lock物件時,是不存在隱式的同步監視器的,所以也就不能使用上面的執行緒通訊方式了。其實在使用Lock物件來保證同步時,Java提供了一個Condition類來保持協調,使用Condition類可以讓那些已經得到Lock物件卻無法繼續執行的執行緒釋放Lock物件。
Condition提供了三個方法:await(),signal(),signallAll();這三個方法和Object物件的三個方法的基本用法是一樣的。其實我們可以這樣認為,Lock物件對應了我們上面講的同步方法或同步程式碼塊,而Condition物件對應了我們上面講的同步監視器。還要注意的是,Condition例項被繫結在一個Lock物件上,要獲得指定Lock的Condition例項,需要呼叫Lock物件的newCondtion()方法即可。
下面使用Lock和Condition的組合來實現生產者消費者問題。可以看到程式碼基本和上面是一樣的。
package lkl1;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
///生產者消費者中的緩衝區
//由一個陣列代表,生產者可以向緩衝區中加入元素,消費者可以從緩衝區中取走元素
//如果緩衝區滿,則生產者不能向緩衝區加入元素;如果緩衝區空,則消費者不能消費元素
//下面的程式中in表示生產者可以加入資料的位置,out表示消費者可以消費資料的位置
//in和out都會初始化為0,我們定義in==out表示緩衝區為空;(in+1)%n==out
//表示緩衝區滿,但是這種判滿的方式是要浪費一個空間的。
//上個例子中使用了synchronized關鍵字保證對緩衝區的操作的同步。
//現在需要採用Lock和Condition類進行同步的控制.
public class Buffer1 {
private final Lock lock=new ReentrantLock();
private final Condition con=lock.newCondition();
private int n;
private int buffer1[];
private int in;
private int out;
private int cnt; ///記錄當前緩衝區中元素個數
Buffer1(){}
Buffer1(int n){
this.n=n;
buffer1=new int[n];
in=out=cnt=0;
}
//生產方法,加入元素x
public void product(int x){
lock.lock(); //加鎖
try{
if((in+1)%n==out){ //如果緩衝區滿,則阻塞當前執行緒
con.await();
//con.signalAll();
}
else{
buffer1[in]=x;
in=(in+1)%n;
cnt++;
System.out.println(Thread.currentThread().getName()+"向緩衝區中加入元素:"+x);
System.out.println("當前緩衝區中的元素個數為: "+cnt);
con.signalAll(); //喚醒其它執行緒
}
}
catch(InterruptedException ex){
ex.printStackTrace();
}
finally{ ///使用finally語句保證鎖能正確釋放
lock.unlock();
}
}
//消費方法,取走緩衝區中的一個元素
public int consumer(){
int x=0;
lock.lock();
try{
if(in==out){ //如果緩衝區空,則阻塞當前執行緒
con.await();
}
else{
x=buffer1[out];
System.out.println(Thread.currentThread().getName()+"消費元素: "+x);
out=(out+1)%n;
cnt--;
System.out.println("當前元素個數為: "+cnt);
con.signalAll(); //喚醒其它執行緒
}
}
catch(InterruptedException ex){
ex.printStackTrace();
}
finally{
lock.unlock();
}
return x;
}
}
package lkl1;
import java.util.Random;
//消費者執行緒
public class Consumer1 extends Thread{
private Random rand=new Random();
private Buffer1 buffer1; //對應的緩衝區
Consumer1(Buffer1 buffer1){
this.buffer1=buffer1;
}
public void run(){
while(true){
buffer1.consumer();
try{ ///在消費者執行緒中加一個sleep語句,可以更好的體現執行緒之間的切換
sleep(50);
}
catch(Exception x){
x.printStackTrace();
}
}
}
}
package lkl1;
import java.util.Random;
//生產者執行緒
public class Product1 extends Thread{
private Random rand = new Random();
private Buffer1 buffer1;
Product1(Buffer1 buffer1){
this.buffer1=buffer1;
}
public void run(){
while(true){
int x;
x=rand.nextInt(100);
buffer1.product(x);
}
}
}
package lkl1;
///Buffer1測試
//啟動一個生產者執行緒,兩個消費者執行緒
public class Buffer1Test {
public static void main(String[] args) throws Exception{
Buffer1 buffer1 = new Buffer1(10);
new Product1(buffer1).start();
new Consumer1(buffer1).start();
new Consumer1(buffer1).start();
}
}
三。使用阻塞佇列(BlockingQueue)來控制執行緒通訊
Java5提供了一個BlockingQueue介面,這個介面也是屬於佇列的子介面,但是他主要的作用還是用來進行執行緒通訊,而不是當成佇列用。BlockingQueue的特徵是:當生產者執行緒試圖向BlockingQueue中加入元素時,如果該佇列已滿,則該執行緒被阻塞;當消費者執行緒試圖從BlockingQueue中取出元素時,如果該佇列為空,則該執行緒會被阻塞。
這樣程式的兩個執行緒通過交替的向BlocingQueue中放入元素,取出元素,即可很好的控制執行緒的通訊。當然也不是所有的BlockingQueue的方法都支援阻塞操作的。
BlockingQueue提供了以下兩個支援阻塞的方法:
put(E e):嘗試把E元素放入BlockingQueue中,如果該佇列的元素已滿,則阻塞該執行緒。
take():嘗試從BlockingQueue的頭部取出元素,如果該佇列的元素為空,則阻塞該執行緒。
其它的Queue對應的方法,也都是支援的,但是在上面的情況下操作,不會阻塞,而是會返回false或丟擲異常。
常用的BlockingQueue的實現類有以下幾種:
ArrayBlockingQueue:基於陣列實現的BlockingQueue佇列
LinkedBlockingQueue:基於連結串列實現的BlockingQueue佇列
PriorityBlockingQueue:優先佇列對應的阻塞佇列
SynchronizedQueue:同步佇列,對該佇列的存取必須交替進行
因為阻塞佇列本身就支援生產者消費者模式,所以用阻塞佇列來實現生產者消費者問題就很簡單了。
package lkl;
import java.util.concurrent.BlockingQueue;
///消費者執行緒
public class Consumer extends Thread{
private BlockingQueue<String>bq;
public Consumer(BlockingQueue<String> bq){
this.bq=bq;
}
public void run(){
while(true){
System.out.println(getName()+"消費者準備消費集合元素!");
try{
Thread.sleep(200);
//嘗試取出元素,如果佇列以空,則執行緒阻塞
bq.take();
}
catch(Exception ex){
ex.printStackTrace();
}
System.out.println(getName()+"消費完成: "+bq);
}
}
}
package lkl;
import java.util.concurrent.BlockingQueue;
//生產者執行緒
public class Producer extends Thread{
private BlockingQueue<String>bq;
public Producer(BlockingQueue bq){
this.bq=bq;
}
public void run(){
String[] strArr = new String[]{
"Java","Struts","Spring"
};
for(int i=0;i<99999999;i++){
System.out.println(getName()+"生產者準備生產集合元素!");
try{
Thread.sleep(200);
//嘗試放入元素,如果佇列已滿,則執行緒會被阻塞
bq.put(strArr[i%3]);
}
catch(Exception ex){
ex.printStackTrace();
}
System.out.println(getName()+"生產完成: "+bq);
}
}
}
package lkl;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueTest2 {
public static void main(String[] args){
//建立一個容量為1的BlockingQueue
BlockingQueue<String> bq=new ArrayBlockingQueue<>(1);
//啟動3個生產者執行緒
new Producer(bq).start();
new Producer(bq).start();
new Producer(bq).start();
//啟動一個消費者執行緒
new Consumer(bq).start();
}
}
相關文章
- python執行緒通訊與生產者消費者模式Python執行緒模式
- java多執行緒:執行緒間通訊——生產者消費者模型Java執行緒模型
- python中多執行緒消費者生產者問題Python執行緒
- Java 多執行緒學習(執行緒通訊——消費者和生產者)Java執行緒
- python 多執行緒實現生產者與消費者模型Python執行緒模型
- java實現生產者消費者問題Java
- 多執行緒併發如何高效實現生產者/消費者?執行緒
- Java多執行緒——生產者消費者示例Java執行緒
- 使用Python佇列和多執行緒實現生產者消費者Python佇列執行緒
- C#多執行緒學習(三) 生產者和消費者C#執行緒
- 生產者消費者問題-C++程式碼實現C++
- Python-多執行緒及生產者與消費者Python執行緒
- 執行緒同步介紹及 生產者消費者問題舉例 C#版執行緒C#
- 生產者與消費者問題
- Java多執行緒程式設計(同步、死鎖、生產消費者問題)Java執行緒程式設計
- java學習回顧---生產者與消費者問題以及多執行緒補充Java執行緒
- Java 多執行緒(Java.Thread)------ 執行緒協作(生產者消費者模式)Java執行緒thread模式
- python多執行緒+生產者和消費者模型+queue使用Python執行緒模型
- Java 多執行緒基礎(十二)生產者與消費者Java執行緒
- Java多執行緒——消費者與生產者的關係Java執行緒
- linux 生產者與消費者問題Linux
- 執行緒間的協作(2)——生產者與消費者模式執行緒模式
- Python並行程式設計(三):多執行緒同步之semaphore(訊號量)實現簡易生產者-消費者模型Python並行行程程式設計執行緒模型
- Java實現生產者和消費者Java
- 面試必問:訊號量與生產者消費者問題!面試
- 生產者消費者模式--java多執行緒同步方法的應用模式Java執行緒
- 訊號量實現生產者消費者(程式碼邏輯有問題,不適合多個消費者,不常用)
- python中多程式消費者生產者問題Python
- java 執行緒池、多執行緒併發實戰(生產者消費者模型 1 vs 10) 附案例原始碼Java執行緒模型原始碼
- 生產者消費者
- 使用BlockQueue實現生產者和消費者模式BloC模式
- 使用Disruptor實現生產者和消費者模型模型
- Thinking in Java---多執行緒學習筆記(2)ThinkingJava執行緒筆記
- Python並行程式設計(六):多執行緒同步之queue(佇列)實現生產者-消費者模型Python並行行程程式設計執行緒佇列模型
- 從一次生產消費者的bug看看執行緒池如何增加執行緒執行緒
- 作業系統—生產者消費者問題詳解作業系統
- 「Kafka應用」PHP實現生產者與消費者KafkaPHP
- 母雞下蛋例項:多執行緒通訊生產者和消費者wait/notify和condition/await/signal條件佇列執行緒AI佇列