四、生產者和消費者
我們這裏的生產者和消費者模型為:
生產者Producer 生產某個對象(共享資源),放在緩沖池中,然後消費者從緩沖池中取出這個對象。也就是生產者生產一個,消費者取出一個。這樣進行循環。
第一步:我們先創建共享資源的類 Person,它有兩個方法,一個生產對象,一個消費對象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class Person {
private String name;
private int age;
/**
* 生產數據
* @param name
* @param age
*/
public void push(String name, int age){
this .name = name;
this .age = age;
}
/**
* 取數據,消費數據
* @return
*/
public void pop(){
System.out.println( this .name+ "---" + this .age);
}
}
|
第二步:創建生產者線程,並在 run() 方法中生產50個對象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class Producer implements Runnable{
//共享資源對象
Person p = null ;
public Producer(Person p){
this .p = p;
}
@Override
public void run() {
//生產對象
for ( int i = 0 ; i < 50 ; i++){
//如果是偶數,那麽生產對象 Tom--11;如果是奇數,則生產對象 Marry--21 if (i% 2 == 0 ){
p.push( "Tom" , 11 );
} else {
p.push( "Marry" , 21 );
}
}
}
}
|
第三步:創建消費者線程,並在 run() 方法中消費50個對象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class Consumer implements Runnable{
//共享資源對象
Person p = null ;
public Consumer(Person p) {
this .p = p;
}
@Override
public void run() {
for ( int i = 0 ; i < 50 ; i++){
//消費對象
p.pop();
}
}
}
|
由於我們的模型是生產一個,馬上消費一個,那期望的結果便是 Tom---11,Marry--21,Tom---11,Mary---21...... 連續這樣交替出現50次
但是結果卻是:
1 2 3 4 5 6 7 8 9 10 11 |
Marry--- 21
Marry--- 21
Marry--- 21
Marry--- 21
Marry--- 21
......
Marry--- 21
Marry--- 21
Marry--- 21
Marry--- 21
Marry--- 21
|
為了讓結果產生的更加明顯,我們在共享資源的 pop() 和 push() 方法中添加一段延時代碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
/**
* 生產數據
* @param name
* @param age
*/
public void push(String name, int age){
this .name = name;
try {
//這段延時代碼的作用是可能只生產了 name,age為nul,消費者就拿去消費了
Thread.sleep( 10 );
} catch (InterruptedException e) {
e.printStackTrace();
}
this .age = age;
}
/**
* 取數據,消費數據
* @return
*/
public void pop(){
try {
Thread.sleep( 10 );
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( this .name+ "---" + this .age);
}
|
這個時候,結果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Marry--- 11
Tom--- 21
Marry--- 11
Tom--- 21
Marry--- 11
Tom--- 21
Marry--- 11
Tom--- 21
......
Tom--- 11
Tom--- 21
Marry--- 11
Tom--- 21
Marry--- 11
Marry--- 21
|
結果分析:這時候我們發現結果全亂套了,Marry--21是固定的,Tom--11是固定的,但是上面的結果全部亂了,那這又是為什麽呢?而且有很多重復的數據連續出現,那這又是為什麽呢?
原因1:出現錯亂數據,是因為先生產出Tom--11,但是消費者沒有消費,然後生產者繼續生產出name為Marry,但是age還沒有生產,而消費者這個時候拿去消費了,那麽便出現 Marry--11。同理也會出現 Tom--21
原因2:出現重復數據,是因為生產者生產一份數據了,消費者拿去消費了,但是第二次生產者生產數據了,但是消費者沒有去消費;而第三次生產者繼續生產數據,消費者才開始消費,這便會產生重復
解決辦法1:生產者生產name和age必須要是一個整體一起完成,即同步。生產的中間不能讓消費者來消費即可。便不會產生錯亂的數據。如何同步可以參考:
Java 多線程詳解(三)------線程的同步:http://www.cnblogs.com/ysocean/p/6883729.html
這裏我們選擇同步方法(在方法前面加上 synchronized)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
public class Person {
private String name;
private int age;
/**
* 生產數據
* @param name
* @param age
*/
public synchronized void push(String name, int age){
this .name = name;
try {
//這段延時代碼的作用是可能只生產了 name,age為nul,消費者就拿去消費了
Thread.sleep( 10 );
} catch (InterruptedException e) {
e.printStackTrace();
}
this .age = age;
}
/**
* 取數據,消費數據
* @return
*/
public synchronized void pop(){
try {
Thread.sleep( 10 );
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( this .name+ "---" + this .age);
}
}
|
結果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Marry--- 21
Marry--- 21
Marry--- 21
Marry--- 21
Marry--- 21
Tom--- 11
Tom--- 11
......
Tom--- 11
Tom--- 11
Tom--- 11
Tom--- 11
Tom--- 11
|
問題:還是沒有解決上面的問題2,出現重復的問題。期望的結果是 Tom---11,Marry--21,Tom---11,Mary---21...... 連續這樣交替出現50次。那如何解決呢?
解決辦法:生產者生產一次數據了,就暫停生產者線程,等待消費者消費;消費者消費完了,消費者線程暫停,等待生產者生產數據,這樣來進行。
這裏我們介紹一個同步鎖池的概念:
同步鎖池:同步鎖必須選擇多個線程共同的資源對象,而一個線程獲得鎖的時候,別的線程都在同步鎖池等待獲取鎖;當那個線程釋放同步鎖了,其他線程便開始由CPU調度分配鎖
關於讓線程等待和喚醒線程的方法,如下:(這是 Object 類中的方法)
wait():執行該方法的線程對象,釋放同步鎖,JVM會把該線程放到等待池中,等待其他線程喚醒該線程
notify():執行該方法的線程喚醒在等待池中等待的任意一個線程,把線程轉到鎖池中等待(註意鎖池和等待池的區別)
notifyAll():執行該方法的線程喚醒在等待池中等待的所有線程,把線程轉到鎖池中等待。
註意:上述方法只能被同步監聽鎖對象來調用,這也是為啥wait() 和 notify()方法都在 Object 對象中,因為同步監聽鎖可以是任意對象,只不過必須是需要同步線程的共同對象即可,否則別的對象調用會報錯: java.lang.IllegalMonitorStateException
假設 A 線程和 B 線程同時操作一個 X 對象,A,B 線程可以通過 X 對象的 wait() 和 notify() 方法來進行通信,流程如下:
①、當線程 A 執行 X 對象的同步方法時,A 線程持有 X 對象的 鎖,B線程在 X 對象的鎖池中等待
②、A線程在同步方法中執行 X.wait() 方法時,A線程釋放 X 對象的鎖,進入 X 對象的等待池中
③、在 X 對象的鎖池中等待鎖的 B 線程獲得 X 對象的鎖,執行 X 的另一個同步方法
④、B 線程在同步方法中執行 X.notify() 方法,JVM 把 A 線程從等待池中移動到 X 對象的鎖池中,等待獲取鎖
⑤、B 線程執行完同步方法,釋放鎖,等待獲取鎖的 A 線程獲得鎖,繼續執行同步方法
那麽為了解決上面重復的問題,修改代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
public class Person {
private String name;
private int age;
//表示共享資源對象是否為空,如果為 true,表示需要生產,如果為 false,則有數據了,不要生產
private boolean isEmpty = true ;
/**
* 生產數據
* @param name
* @param age
*/
public synchronized void push(String name, int age){
try {
//不能用 if,因為可能有多個線程
while (!isEmpty){ //進入到while語句內,說明 isEmpty==false,那麽表示有數據了,不能生產,必須要等待消費者消費
this .wait(); //導致當前線程等待,進入等待池中,只能被其他線程喚醒
}
//-------生產數據開始-------
this .name = name;
//延時代碼
Thread.sleep( 10 );
this .age = age;
//-------生產數據結束-------
isEmpty = false ; //設置 isEmpty 為 false,表示已經有數據了
this .notifyAll(); //生產完畢,喚醒所有消費者
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 取數據,消費數據
* @return
*/
public synchronized void pop(){
try {
//不能用 if,因為可能有多個線程
while (isEmpty){ //進入 while 代碼塊,表示 isEmpty==true,表示為空,等待生產者生產數據,消費者要進入等待池中
this .wait(); //消費者線程等待
}
//-------消費開始-------
Thread.sleep( 10 );
System.out.println( this .name+ "---" + this .age);
//-------消費結束------
isEmpty = true ; //設置 isEmpty為true,表示需要生產者生產對象
this .notifyAll(); //消費完畢,喚醒所有生產者
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
|
結果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Tom--- 11
Marry--- 21
Tom--- 11
Marry--- 21
Tom--- 11
Marry--- 21
Tom--- 11
......
Marry--- 21
Tom--- 11
Marry--- 21
Tom--- 11
Marry--- 21
Tom--- 11
Marry--- 21
|
那麽這便是我們期待的結果,交替出現。
死鎖:
①、多線程通信的時候,很容易造成死鎖,死鎖無法解決,只能避免
②、當 A 線程等待由 B 線程持有的鎖,而 B 線程正在等待由 A 線程持有的鎖時發生死鎖現象(比如A拿著鉛筆,B拿著圓珠筆,A說你先給我圓珠筆,我就把鉛筆給你,而B說你先給我鉛筆,我就把圓珠筆給你,這就造成了死鎖,A和B永遠不能進行交換)
③、JVM 既不檢測也不避免這種現象,所以程序員必須保證不能出現這樣的情況
Thread 類中容易造成死鎖的方法(這兩個方法都已經過時了,不建議使用):
suspend():使正在運行的線程放棄 CPU,暫停運行(不釋放鎖)
resume():使暫停的線程恢復運行
情景:A 線程獲得對象鎖,正在執行一個同步方法,如果 B線程調用 A 線程的 suspend() 方法,此時A 暫停運行,放棄 CPU 資源,但是不放棄同步鎖,那麽B也不能獲得鎖,A又暫停,那麽便造成死鎖。
解決死鎖法則:當多個線程需要訪問 共同的資源A,B,C時,必須保證每一個線程按照一定的順序去訪問,比如都先訪問A,然後B,最後C。就像我們這裏的生產者---消費者模型,制定了必須生產者先生產一個對象,然後消費者去消費,消費完畢,生產者才能在開始生產,然後消費者在消費。這樣的順序便不會造成死鎖。
四、生產者和消費者