java並發之線程間通信協作


  在前面我們將了很多關於同步的問題,然而在現實中,需要線程之間的協作。比如說最經典的生產者-消費者模型:當隊列滿時,生產者需要等待隊列有空間才能繼續往里面放入商品,而在等待的期間內,生產者必須釋放對臨界資源(即隊列)的占用權。因為生產者如果不釋放對臨界資源的占用權,那么消費者就無法消費隊列中的商品,就不會讓隊列有空間,那么生產者就會一直無限等待下去。因此,一般情況下,當隊列滿時,會讓生產者交出對臨界資源的占用權,並進入掛起狀態。然后等待消費者消費了商品,然后消費者通知生產者隊列有空間了。同樣地,當隊列空時,消費者也必須等待,等待生產者通知它隊列中有商品了。這種互相通信的過程就是線程間的協作。

  Java中線程通信協作的最常見的兩種方式:

  一.syncrhoized加鎖的線程的Object類的wait()/notify()/notifyAll()

  二.ReentrantLock類加鎖的線程的Condition類的await()/signal()/signalAll()

  線程間直接的數據交換:

  三.通過管道進行線程間通信:1)字節流;2)字符流   

一.syncrhoized加鎖的線程的Object類的wait()/notify()/notifyAll()

wait()、notify()和notifyAll()是Object類中的方法:

 1 /**
 2  * Wakes up a single thread that is waiting on this object's
 3  * monitor. If any threads are waiting on this object, one of them
 4  * is chosen to be awakened. The choice is arbitrary and occurs at
 5  * the discretion of the implementation. A thread waits on an object's
 6  * monitor by calling one of the wait methods
 7  */
 8 public final native void notify();
 9  
10 /**
11  * Wakes up all threads that are waiting on this object's monitor. A
12  * thread waits on an object's monitor by calling one of the
13  * wait methods.
14  */
15 public final native void notifyAll();
16  
17 /**
18  * Causes the current thread to wait until either another thread invokes the
19  * {@link java.lang.Object#notify()} method or the
20  * {@link java.lang.Object#notifyAll()} method for this object, or a
21  * specified amount of time has elapsed.
22  * <p>
23  * The current thread must own this object's monitor.
24  */
25 public final native void wait(long timeout) throws InterruptedException;
View Code

從這三個方法的文字描述可以知道以下幾點信息:

  1)wait()、notify()和notifyAll()方法是本地方法,並且為final方法,無法被重寫。

  2)調用某個對象的wait()方法能讓當前線程阻塞,並且當前線程必須擁有此對象的monitor(即鎖)

  3)調用某個對象的notify()方法能夠喚醒一個正在等待這個對象的monitor的線程,如果有多個線程都在等待這個對象的monitor,則只能喚醒其中一個線程

  4)調用notifyAll()方法能夠喚醒所有正在等待這個對象的monitor的線程

  有朋友可能會有疑問:為何這三個不是Thread類聲明中的方法,而是Object類中聲明的方法(當然由於Thread類繼承了Object類,所以Thread也可以調用者三個方法)?其實這個問題很簡單,由於每個對象都擁有monitor(即鎖),所以讓當前線程等待某個對象的鎖,當然應該通過這個對象來操作了。而不是用當前線程來操作,因為當前線程可能會等待多個線程的鎖,如果通過線程來操作,就非常復雜了。

  上面已經提到,如果調用某個對象的wait()方法,當前線程必須擁有這個對象的monitor(即鎖)因此調用wait()方法必須在同步塊或者同步方法中進行(synchronized塊或者synchronized方法)如果當前線程沒有這個對象的鎖就調用wait()方法,則會拋出IllegalMonitorStateException.

   調用某個對象的wait()方法,相當於讓當前線程交出(釋放)此對象的monitor,然后進入等待狀態,等待后續再次獲得此對象的鎖(Thread類中的sleep方法使當前線程暫停執行一段時間,從而讓其他線程有機會繼續執行,但它並不釋放對象鎖);  

  notify()方法能夠喚醒一個正在等待該對象的monitor的線程,當有多個線程都在等待該對象的monitor的話,則只能喚醒其中一個線程,具體喚醒哪個線程則不得而知。 同樣地,調用某個對象的notify()方法,當前線程也必須擁有這個對象的monitor,因此調用notify()方法必須在同步塊或者同步方法中進行(synchronized塊或者synchronized方法)。

  nofityAll()方法能夠喚醒所有正在等待該對象的monitor的線程,這一點與notify()方法是不同的。

  這里要注意一點:notify()和notifyAll()方法只是喚醒等待該對象的monitor的線程,並不決定哪個線程能夠獲取到monitor。

   舉個簡單的例子:假如有三個線程Thread1、Thread2和Thread3都在等待對象objectA的monitor,此時Thread4擁有對象objectA的monitor,當在Thread4中調用objectA.notify()方法之后,Thread1、Thread2和Thread3只有一個能被喚醒。注意,被喚醒不等於立刻就獲取了objectA的monitor。假若在Thread4中調用objectA.notifyAll()方法,則Thread1、Thread2和Thread3三個線程都會被喚醒至於哪個線程接下來能夠獲取到objectA的monitor就具體依賴於操作系統的調度了

  上面尤其要注意一點,一個線程被喚醒不代表立即獲取了對象的monitor,只有等調用完notify()或者notifyAll()並退出synchronized塊,釋放對象鎖后,其余線程才可獲得鎖執行

 一個生產者一個消費者

一個對象,作為鎖(利用該對象的monitor)

1 package com.jp.oneone;
2 
3 public class ValueObject {
4 
5     public static String value = "";
6 
7 }

生產者:

 1 package com.jp.oneone;
 2 
 3 //生產者
 4 public class P extends Thread{
 5 
 6     private String lock;
 7 
 8     public P(String lock) {
 9         super();
10         this.lock = lock;
11     }
12     
13     @Override
14     public void run() {
15         while (true) {
16             try {
17                 synchronized (lock) { //當前線程必須獲得鎖才可以進行下面的操作
18                     if (!ValueObject.value.equals("")) {//如果Value不為空,說明字符串還沒被消費,所以調用wait方法,把當前線程(生成線程)阻塞
19                         lock.wait();
20                     }
21                     String value = System.currentTimeMillis() + "_"
22                             + System.nanoTime();
23                     System.out.println("set的值是" + value);
24                     ValueObject.value = value;//為空的話,則生成
25                     lock.notify();//生成完就喚醒等待該對象鎖的線程,(這里只有一個消費者等這個鎖,所以就是喚醒的它)
26                 }
27 
28             } catch (InterruptedException e) {
29                 e.printStackTrace();
30             }
31         }
32     }
33 }

消費者:

package com.jp.oneone;

//消費者
public class C extends Thread {

    private String lock;

    public C(String lock) {
        super();
        this.lock = lock;
    }
    
    @Override
    public void run() {
        while (true) {
            try {
                synchronized (lock) {
                    if (ValueObject.value.equals("")) {//如果字符串為空,即被消費完了,所以wait等待。
                        lock.wait();
                    }
                    System.out.println("get的值是" + ValueObject.value);
                    ValueObject.value = "";
                    lock.notify();
                }

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

測試:

package com.jp.oneone;

public class Run {

    public static void main(String[] args) {

        String lock = new String("");
        P p = new P(lock);
        C r = new C(lock);

        p.start();
        r.start();
    }

}

運行結果

本例是1個生產者1個消費者進行數據的交互。

 多個生產者多個消費者

實現和上面1對1基本一樣,只是在測試代碼中,多new幾個生產者,幾個消費者。

只需注意一個問題:假死

問題描述:所有線程都被wait,這個項目就停止運行了。

問題原因:代碼中使用wait/notify進行通信,不能保證notify喚醒的是異類(生產者喚醒消費者還是生產者),比如生產者喚醒生產者,消費者喚醒消費者,就可能導致都在等待的狀態。

問題解決:其實很簡單,就是喚醒的時候同類異類都喚醒,把notify()改為natifyAll()就解決了。

二.ReentrantLock類加鎖的線程的Condition類的await()/signal()/signalAll()

  Condition是在java 1.5中才出現的,它用來替代傳統的Object的wait()、notify()實現線程間的協作,相比使用Object的wait()、notify(),使用Condition1的await()、signal()這種方式實現線程間協作更加安全和高效。因此通常來說比較推薦使用Condition,在阻塞隊列那一篇博文中就講述到了,阻塞隊列實際上是使用了Condition來模擬線程間協作。

  • Condition是個接口,基本的方法就是await()和signal()方法;
  • Condition依賴於Lock接口,生成一個Condition的基本代碼是lock.newCondition() 
  •  調用Condition的await()和signal()方法,都必須在lock保護之內,就是說必須在lock.lock()和lock.unlock之間才可以使用

  Conditon中的await()對應Object的wait();

  Condition中的signal()對應Object的notify();

  Condition中的signalAll()對應Object的notifyAll()。

 一個生產者一個消費者

 這個例子用了《Java多線程編程核心技術》中的方式,把生產者和消費者的方法寫到一個類中,與生成線程和消費線程分開,感覺更高大上,當然上面的例子也可以寫成這種方式。

含有生成者消費者的類

 1 package oneone;
 2 
 3 import java.util.concurrent.locks.Condition;
 4 import java.util.concurrent.locks.ReentrantLock;
 5 
 6 public class MyService {
 7 
 8     private ReentrantLock lock = new ReentrantLock();//拿到可重入鎖,相當於synchronized的作用
 9     private Condition condition = lock.newCondition();//調用await和signal方法的對象,相當於Object對象(任意對象)的的wait和notify方法
10     private boolean hasValue = false;
11 
12     //生產者
13     public void set() {
14         try {
15             lock.lock();//獲得鎖
16             while (hasValue == true) {
17                 condition.await(); //沒被消費則阻塞該生產線程,當然也釋放了鎖,進入等鎖的隊列
18             }
19             System.out.println("打印★");
20             hasValue = true;
21             condition.signal();
22         } catch (InterruptedException e) {
23             e.printStackTrace();
24         } finally {
25             lock.unlock();
26         }
27     }
28     
29     //消費者
30     public void get() {
31         try {
32             lock.lock();
33             while (hasValue == false) {
34                 condition.await();
35             }
36             System.out.println("打印☆");
37             hasValue = false;
38             condition.signal();
39         } catch (InterruptedException e) {
40             e.printStackTrace();
41         } finally {
42             lock.unlock();
43         }
44     }
45 
46 }

對應生成者的線程類

package oneone;

public class MyThreadA extends Thread {

    private MyService myService;

    public MyThreadA(MyService myService) {
        super();
        this.myService = myService;
    }

    @Override
    public void run() {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            myService.set();
        }
    }

}

對應消費者的線程類

package oneone;


public class MyThreadB extends Thread {

    private MyService myService;

    public MyThreadB(MyService myService) {
        super();
        this.myService = myService;
    }

    @Override
    public void run() {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            myService.get();
        }
    }

}

測試類

package oneone;

public class Run {

    public static void main(String[] args) throws InterruptedException {
        MyService myService = new MyService();

        MyThreadA a = new MyThreadA(myService);
        a.start();

        MyThreadB b = new MyThreadB(myService);
        b.start();

    }
}

運行結果

 三.通過管道進行線程間通信:1)字節流;2)字符流

 Java中有各種各樣的輸入、輸出流(Stream),其中管道流(pipeStream)是一種特殊的流,用於在不同線程間直接傳送數據。

 一個線程發送數據到輸出管道,另一個線程從輸入管道讀數據。

以字節流舉例:

寫數據的類,把數據寫入管道輸出流

package pipeInputOutput;

import java.io.IOException;
import java.io.PipedOutputStream;

public class WriteData {

    public void writeMethod(PipedOutputStream out) {
        try {
            System.out.println("write :");
            for (int i = 0; i < 300; i++) {
                String outData = "" + (i + 1);
                out.write(outData.getBytes());
                System.out.print(outData);
            }
            System.out.println();
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

讀數據的類,從管道輸入流讀數據

package pipeInputOutput;

import java.io.IOException;
import java.io.PipedInputStream;

public class ReadData {

    public void readMethod(PipedInputStream input) {
        try {
            System.out.println("read  :");
            byte[] byteArray = new byte[20];
            int readLength = input.read(byteArray);
            while (readLength != -1) {
                String newData = new String(byteArray, 0, readLength);
                System.out.print(newData);
                readLength = input.read(byteArray);
            }
            System.out.println();
            input.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

寫數據的線程類

package pipeInputOutput;

import java.io.PipedOutputStream;

public class ThreadWrite extends Thread {

    private WriteData write;
    private PipedOutputStream out;

    public ThreadWrite(WriteData write, PipedOutputStream out) {
        super();
        this.write = write;
        this.out = out;
    }

    @Override
    public void run() {
        write.writeMethod(out);
    }

}

讀數據的線程類

package pipeInputOutput;

import java.io.PipedInputStream;

public class ThreadRead extends Thread {

    private ReadData read;
    private PipedInputStream input;

    public ThreadRead(ReadData read, PipedInputStream input) {
        super();
        this.read = read;
        this.input = input;
    }

    @Override
    public void run() {
        read.readMethod(input);
    }
}

測試類:

package pipeInputOutput;

import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;

public class Run {

    public static void main(String[] args) {

        try {
            WriteData writeData = new WriteData();
            ReadData readData = new ReadData();

            PipedInputStream inputStream = new PipedInputStream();
            PipedOutputStream outputStream = new PipedOutputStream();

            //將兩個Stream之間產生通信鏈接,這樣才能將數據進行輸入輸出,下面兩種方式都可以,其一即可
            //inputStream.connect(outputStream);
            outputStream.connect(inputStream);

            //開啟讀線程
            ThreadRead threadRead = new ThreadRead(readData, inputStream);
            threadRead.start();

            Thread.sleep(2000);

            //開啟寫線程
            ThreadWrite threadWrite = new ThreadWrite(writeData, outputStream);
            threadWrite.start();

        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

}

運行結果

read  :
write :
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300

 

 

《Java多線程編程核心技術》

http://www.cnblogs.com/dolphin0520/p/3920385.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM