文章目錄
多線程間通信
多線程通信其實包含兩方面,一個是線程間通知,一個線程告訴另一個線程是否結束。另一個是線程間傳值。
1. java線程間直接傳值
而java多線程之間的通信他不像Erlang語言那種沒有共享變量,每個線程都操作單獨變量然後傳遞值給其他線程。java這種操作共享變量機制就要加同步鎖來解決一系列安全問題。
所以說Erlang在操作多線程更安全是很有道理的。
java也提供不同線程間傳值
PipedInputStream類 與 PipedOutputStream類 用於在應用程序中創建管道通信。一個PipedInputStream實例對象必須和一個PipedOutputStream實例對象進行連接而產生一個通信管道。
在 Java 的 JDK 中,提供了四個類用於線程間通信傳值:
- 字節流:PipedInputStream 和 PipedOutputStream;
- 字符流:PipedReader 和 PipedWriter;
PipedOutputStream可以向管道中寫入數據,PipedIntputStream可以讀取PipedOutputStream向管道中寫入的數據,這兩個類主要用來完成線程之間的通信。一個線程的PipedInputStream對象能夠從另外一個線程的PipedOutputStream對象中讀取數據,如下圖所示:
但感覺實際上用到的情況比較多的是線程操作數據而不是儲存數據,所以數據都存儲在主線程內存中,其他線程來操作就可以了。
2. 等待、喚醒機制
等待喚醒機制主要是一個線程通知其他線程是否來進入操作。
JDK5之前主要實現方式是使用synchronized 和notify 、wait方法。但是存在一些缺陷,JDK5是一個改動很大的版本,之後使用Lock鎖來保證同步 和await、notify方法來實現代替之前版本。
我們通過java多線程範例生產者消費者模式來介紹這兩種方式。
synchronized + wait + notify
之前我們的例子比如說銀行存錢,每個線程都是執行相同的代碼,都是向銀行中存錢。
而通常情況我們是要不同的線程執行不同的代碼做不同的事,比如說一個線程往銀行存錢,一個線程從銀行取錢。我們現在用一個經典的生產者消費者例子來介紹多線程。
如何使用共享對象:
首先我們介紹一下如何使用共享對象,這裏我們使用共享對象的時候可以這樣做:這裏相當於把主線程的變量傳遞給子線程,子線程就可以修改該變量。
//自定義共享對象
Res r = new Res();
//定義兩個線程對象把共享對象傳入構造方法
Input in = new Input(r);
Output out = new Output(r);
//開啓線程
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
t1.start();
t2.start();
生產者消費者模型:例子說明
我們生產麪包,生產1消費1,生產2消費2,也就是說我們的生產和消費要一一對應。並且生產和消費在不同線程執行。
而且每生產一個就要消費一個這樣不造成資源的浪費。也就是說生產和消費一一對應。
生產消費者模型:安全問題
//共享資源類
class Resource {
private int breadCount = 0; // 資源編號
// 生產資源
public void produce(String name) {
synchronized (this) {
breadCount++; // 資源編號遞增,用來模擬資源遞增
System.out.println(Thread.currentThread().getName() + "...生產者生產bread.." + breadCount);
}
}
// 消費資源
public void consume() {
System.out.println(Thread.currentThread().getName() + "...消費者消費bread......." + breadCount);
}
}
// 生產者類線程
class Producer implements Runnable {
private Resource res;
//構造函數中生產者初始化分配資源
public Producer(Resource res) {
this.res = res;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
res.produce("bread"); // 循環生產10次
}
}
}
// 消費者類線程
class Comsumer implements Runnable {
private Resource res;
//構造函數中消費者一初始化也要分配資源
public Comsumer(Resource res) {
this.res = res;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
res.consume(); // 循環消費10次
}
}
}
public class SynchronizedSample {
public static void main(String[] args) {
Resource resource = new Resource(); // 實例化資源
Producer producer = new Producer(resource); // 實例化生產者和消費者類,它們取得同一個資源
Comsumer comsumer = new Comsumer(resource);
Thread threadProducer = new Thread(producer); // 創建1個生產者線程
Thread threadComsumer = new Thread(comsumer); // 創建1個消費者線程
// 分別開啓線程
threadProducer.start();
threadComsumer.start();
}
}
/*
Thread-0...生產者生產..bread1
Thread-0...生產者生產..bread2
Thread-0...生產者生產..bread3
Thread-1...消費者消費.......bread1
Thread-1...消費者消費.......bread4
Thread-1...消費者消費.......bread4
...*/
我們可以發現由於我們沒有加任何同步機制由於使用共享資源,多線程所以必然會出現安全問題,這裏同時消費多個bread4
生產消費者模型:加同步鎖
由於上述問題,我們需要解決,因爲我們在生產資源的時候,比如生產bread2,這時只有生產線程在操作,消費線程是不能操作的。也就是說保證生產一個麪包的時候只有一個線程在操作該面包。
我們查找哪塊共享資源是多線程要操作,然後修改:
// 資源類
class Resource {
private int breadCount = 0; // 資源編號
// 生產資源
public void produce(String name) {
synchronized (this) {
breadCount++; // 資源編號遞增,用來模擬資源遞增
System.out.println(Thread.currentThread().getName() + "...生產者生產bread.." + breadCount);
}
}
// 消費資源
public void consume() {
synchronized (this) {
System.out.println(Thread.currentThread().getName() + "...消費者消費bread......." + breadCount);
}
}
}
/*
Thread-0...生產者生產bread..1
Thread-0...生產者生產bread..2
Thread-0...生產者生產bread..3
Thread-0...生產者生產bread..4
Thread-0...生產者生產bread..5
Thread-0...生產者生產bread..6
Thread-0...生產者生產bread..7
Thread-0...生產者生產bread..8
Thread-0...生產者生產bread..9
Thread-0...生產者生產bread..10
Thread-1...消費者消費bread.......10
Thread-1...消費者消費bread.......10
Thread-1...消費者消費bread.......10
Thread-1...消費者消費bread.......10
Thread-1...消費者消費bread.......10
Thread-1...消費者消費bread.......10
Thread-1...消費者消費bread.......10
Thread-1...消費者消費bread.......10
Thread-1...消費者消費bread.......10
Thread-1...消費者消費bread.......10*/
我們在共享資源的地方添加了同步塊,也就是說,當我們每生產一個麪包的時候,不能消費該面包。但是又出現個問題就是我們的生產者線程:
// 生產者類線程
class Producer implements Runnable {
private Resource res;
//構造函數中生產者初始化分配資源
public Producer(Resource res) {
this.res = res;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
res.produce("bread"); // 循環生產10次
}
}
}
在生產者線程中循環10次調用produce同步鎖,而一直沒退出該線程,只有當退出該線程時纔會釋放同步鎖,而我們一直沒有退出該線程就沒釋放該鎖,鎖只有退出該線程時纔會釋放。所以生產者線程一直拿着這個鎖生產了10個bread而消費者不能消費,等到生產完10個才進行消費,這不是我們目標,我們要求生產一個就消費一個,不然之前生產的麪包可能會過期。
生產消費者模型:等待喚醒機制
爲解決上述問題,我們可以爲資源設置一個標誌flag,該標誌用來標明資源是否存在,所有的線程執行操作前都要判斷資源是否存在。系統初始化後,資源是空的。如果是消費者線程獲得執行權,先判斷資源,此時爲空,就會進入凍結狀態,交出執行權,並喚醒其他線程。當生產者線程獲得執行權,先判斷資源,若爲空,立馬進行生產,生產完成進入凍結交出執行權並喚醒其他線程。
線程間通知:通過等待喚醒實現:
wait()讓線程進入凍結狀態,交出執行權(釋放鎖)
notify() 喚醒一個凍結狀態的線程(持有相同鎖的線程)
notifyAll() 喚醒所有凍結狀態線程(持有相同鎖的)
這樣的方法都用在同步裏,因爲需要鎖,用鎖的對象來調用這些方法,比如這個鎖是r,那麼就使用r.wait().表示持有r這個鎖的線程。所以只有同一個鎖上的被等待線程可以被同一個鎖的r.notify()喚醒。不能喚醒持有其他所的線程。
但是如果有多個線程,比如多個生產線程和多個消費線程,notifyAll()會全部喚醒他們。遺憾的是,並不能直接喚醒對方線程比如只喚醒消費者。這點也就是他的缺陷,也就是後面一章我們講的JDK5的版本升級,對synchronized和wait模式的替換。
// 資源類
class Resource {
private int breadCount = 0; // 資源編號
private boolean flag = false; // 資源類增加一個資源標誌位,判斷是否有資源
// 生產資源
public void produce() {
synchronized (this) {
// 添加循環判斷,如果flag爲true,也就是有資源了,生產者線程就暫停生產,進入凍結狀態,等待喚醒。
while (flag == true) {
try {
this.wait(); // wait函數拋出的異常只能被截獲
// 因爲wait會讓該線程等在這裏,如果這裏使用if判斷,則當線程被喚醒後會直接往下執行,
// 不再進行flag判斷了,則由於錯誤標記執行,這就可能造成多線程死鎖。
//所以使用while循環判斷,讓線程再次判斷是否標記正確
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//當可以生產的時候flag=false時,執行以下
breadCount++; // 資源編號遞增,用來模擬資源遞增
System.out.println(Thread.currentThread().getName() + "...生產者生產bread.." + breadCount);
//生產完成修改flag
flag = true;
//並喚醒其他所有線程,如果只喚醒一個,則可能喚醒一個同類線程,而我們要喚醒的是對方線程
this.notifyAll();
}
}
// 消費資源
public void consume() {
synchronized (this) {
//判斷如果沒有資源則消費者等待
while (flag == false) {
try {
this.wait(); // wait函數拋出的異常只能被截獲
// 因爲wait會讓該線程等在這裏,如果這裏使用if判斷,則當線程被喚醒後會直接往下執行,
// 不再進行flag判斷了,則由於錯誤標記執行,這就可能造成多線程死鎖。
//所以使用while循環判斷,讓線程再次判斷是否標記正確
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "...消費者消費bread......." + breadCount);
//生產完成修改flag
flag = false;
//並喚醒其他所有線程,如果只喚醒一個,則可能喚醒一個同類線程,而我們要喚醒的是對方線程
this.notifyAll();
}
}
}
// 生產者類線程
class Producer implements Runnable {
private Resource res;
//構造函數中生產者初始化分配資源
public Producer(Resource res) {
this.res = res;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
res.produce(); // 每個線程生產5個
}
}
}
// 消費者類線程
class Comsumer implements Runnable {
private Resource res;
//構造函數中消費者一初始化也要分配資源
public Comsumer(Resource res) {
this.res = res;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
res.consume(); // 每個線程消費5個
}
}
}
public class SynchronizedSample {
public static void main(String[] args) {
Resource resource = new Resource(); // 實例化資源
Thread threadProducer = new Thread(new Producer(resource)); // 創建2個生產者線程
Thread threadProducer2 = new Thread(new Producer(resource)); // 創建2個生產者線程
Thread threadComsumer = new Thread(new Comsumer(resource)); // 創建2個消費者線程
Thread threadComsumer2 = new Thread(new Comsumer(resource)); // 創建2個消費者線程
// 分別開啓線程
threadProducer.start();
threadProducer2.start();
threadComsumer.start();
threadComsumer2.start();
}
}
/*
Thread-0...生產者生產bread..1
Thread-2...消費者消費bread.......1
Thread-1...生產者生產bread..2
Thread-3...消費者消費bread.......2
Thread-0...生產者生產bread..3
Thread-2...消費者消費bread.......3
Thread-1...生產者生產bread..4
Thread-3...消費者消費bread.......4
Thread-0...生產者生產bread..5
Thread-2...消費者消費bread.......5
Thread-1...生產者生產bread..6
Thread-3...消費者消費bread.......6
Thread-0...生產者生產bread..7
Thread-2...消費者消費bread.......7
Thread-1...生產者生產bread..8
Thread-2...消費者消費bread.......8
Thread-0...生產者生產bread..9
Thread-3...消費者消費bread.......9
Thread-1...生產者生產bread..10
Thread-3...消費者消費bread.......10
*/
根據結果可以看出:不同線程負責生產和消費,當生產一個就消費一個。運行正確。
進入 wait()方法後,當前線程釋放鎖。然後當我們notifyAll()喚醒其他線程的時候,誰競爭到這個對象鎖誰就獲得執行權,進行執行,執行完後再進入wait()等待並釋放鎖,讓其他線程執行。
上述代碼中的問題有2點需要注意,
- 第一點是用if還是while來判斷flag,這點在代碼註釋中已經說明,
- 第二點是用notify還是notifyAll函數。也在代碼註釋中說明
所以,多線程一般都要用while和notifyAll()的組合。
小結
多線程編程往往是多個線程執行不同的任務,不同的任務不僅需要“同步”,還需要“等待喚醒機制”。兩者結合就可以實現多線程編程,其中的生產者消費者模式就是經典範例。
然而,使用synchronized修飾同步函數和使用Object類中的wait,notify方法實現等待喚醒是有弊端的。就是效率問題,notifyAll方法喚醒所有被wait的線程,包括本類型的線程,如果本類型的線程被喚醒,還要再次判斷並進入wait,這就產生了很大的效率問題,也在代碼中給出了說明。理想狀態下,生產者線程要喚醒消費者線程,而消費者線程要喚醒生產者線程。爲此,JDK5提供了Lock和Condition接口及實現類來替代sychronized和wait機制,將在下一章介紹。