[TOC]
*****
# 23.1 基礎知識
## 23.1.1 進程
**一個進程**就是一個執行中的程序,而每一個進程都有自己獨立的一塊內存空間、一組系統資源。在進程的概念中,每一個進程的內部數據和狀態都是完全獨立的。
在Windows操作系統下可以通過Ctrl+Alt+Del組合鍵查看進程,在UNIX和Linux操作系統下是通過ps命令查看進程的。打開Windows當前運行的進程,如圖23-1所示。

在Windows操作系統中一個進程就是一個exe或者dll程序,它們相互獨立,互相也可以通信,在Android操作系統中進程間也可以通信。
## 23.1.2 線程
一個進程中可以包含多個線程,線程是一段完成某個特定功能的代碼 ,是程序中單個順序控制的流程,同類的多個線程是共享一塊內存空間和一組系統資源。所以系統在各個線程之間切換時,開銷要比進程小的多,線程被稱為輕量級進程。
## 23.1.3 主線程
Java程序至少會有一個線程,這就是主線程,程序啟動后是由JVM創建主線程,程序結束時由JVM停止主線程。主線程它負責管理子線程,即子線程的啟動、掛起、停止等等操作。圖23-2所示是進程、主線程和子線程的關系,其中主線程負責管理子線程,即子線程的啟動、掛起、停止等操作。

**圖23-2 進程、主線程和子線程關系**
獲取主線程示例代碼如下:
```
//HelloWorld.java文件
package lianl;
public class HelloWorld {
public static void main(String[] args) {
//獲取主線程,在main方法中
Thread mainThread = Thread.currentThread();
System.out.println("主線程名:" + mainThread.getName());
}
}
```
Java中創建一個子線程涉及到:java.lang.Thread類和java.lang.Runnable接口。
**Thread線程類:** 創建一個Thread對象就會產生一個新的線程。
**線程執行對象:** 實現Runnable接口的對象,線程執行的代碼是實現Runnable接口對象重寫run()方法中的代碼。
**提示**
**主線程中執行入口**是main(String\[\] args)方法,主線程可以控制程序的流程,管理其他的子線程。
**子線程執行入口**是線程執行對象的run()方法
# 23.2.1 實現Runnable接口
創建線程Thread對象時,可以將線程執行對象傳遞給它,這需要是使用Thread類如下兩個構造方法:
* Thread(Runnable target, String name):target是線程執行對象,實現Runnable接口。name為線程指定一個名字。
* Thread(Runnable target):target是線程執行對象,實現Runnable接口。線程名字是由JVM分配的。
*****
**下面看一個具體示例,實現Runnable接口的線程執行對象Runner代碼如下:**
```
//Runner.java文件
package com.a51work6;
//線程執行對象
public class Runner implements Runnable { ①
// 編寫執行線程代碼
@Override
public void run() { ②
for (int i = 0; i < 10; i++) {
// 打印次數和線程的名字
System.out.printf("第 %d次執行 - %s\n", i,
Thread.currentThread().getName()); ③
try {
// 隨機生成休眠時間
long sleepTime = (long) (1000 * Math.random());
// 線程休眠
Thread.sleep(sleepTime); ④
} catch (InterruptedException e) {
}
}
// 線程執行結束
System.out.println("執行完成! " + Thread.currentThread().getName());
}
}
```
上述代碼第①行聲明實現Runnable接口,這要覆蓋代碼第②行的run()方法。run()方法是線程體,在該方法中編寫你自己的線程處理代碼。
本例中線程體中進行了十次循環,每次讓當前線程休眠一段時間。其中代碼第③行是打印次數和線程的名字,Thread.currentThread()可以獲得當前線程對象,getName()是Thread類的實例方法,可以獲得線程的名。代碼第④行Thread.sleep(sleepTime)是休眠當前線程, sleep是靜態方法它有兩個版本:
* static void sleep(long millis):在指定的毫秒數內讓當前正在執行的線程休眠。
* static void sleep(long millis, int nanos) 在指定的毫秒數加指定的納秒數內讓當前正在執行的線程休眠。
**測試程序HelloWorld代碼如下:**
```
//Runner.java文件
package com.a51work6;
//線程執行對象
public class Runner implements Runnable { ①
// 編寫執行線程代碼
@Override
public void run() { ②
for (int i = 0; i < 10; i++) {
// 打印次數和線程的名字
System.out.printf("第 %d次執行 - %s\n", i,
Thread.currentThread().getName()); ③
try {
// 隨機生成休眠時間
long sleepTime = (long) (1000 * Math.random());
// 線程休眠
Thread.sleep(sleepTime); ④
} catch (InterruptedException e) {
}
}
// 線程執行結束
System.out.println("執行完成! " + Thread.currentThread().getName());
}
}
```
一臺PC通常就只有一顆CPU,在某個時刻只能是一個線程在運行,而Java語言在設計時就充分考慮到線程的并發調度執行。對于程序員來說,在編程時要注意給每個線程執行的時間和機會,主要是通過讓線程休眠的辦法(調用sleep()方法)來讓當前線程暫停執行,然后由其他線程來爭奪執行的機會。如果上面的程序中沒有用到sleep()方法,則就是第一個線程先執行完畢,然后第二個線程再執行完畢。所以用活sleep()方法是多線程編程的關鍵。
# 23.3 線程的狀態
* 新建狀態
新建狀態(New)是通過new等方式創建線程對象,它僅僅是一個空的線程對象。
* 就緒狀態
當主線程調用新建線程的start()方法后,它就進入就緒狀態(Runnable)。此時的線程尚未真正開始執行run()方法,它必須等待CPU的調度。
* 運行狀態
CPU的調度就緒狀態的線程,線程進入運行狀態(Running),處于運行狀態的線程獨占CPU,執行run()方法。
* 阻塞狀態
因為某種原因運行狀態的線程會進入不可運行狀態,即阻塞狀態(Blocked),處于阻塞狀態的線程JVM系統不能執行該線程,即使CPU空閑,也不能執行該線程。如下幾個原因會導致線程進入阻塞狀態:
* 當前線程調用sleep()方法,進入休眠狀態。
* 被其他線程調用了join()方法,等待其他線程結束。
* 發出I/O請求,等待I/O操作完成期間。
* 當前線程調用wait()方法。
處于阻塞狀態可以重新回到就緒狀態,如:休眠結束、其他線程加入、I/O操作完成和調用notify或notifyAll喚醒wait線程。
* 死亡狀態
線程退出run()方法后,就會進入死亡狀態(Dead),線程進入死亡狀態有可以是正常實現完成run()方法進入,也可能是由于發生異常而進入的。

# 23.4 線程管理
## 23.4.1 線程優先級
線程的調度程序根據線程決定每次線程應當何時運行,Java提供了10種優先級,分別用1~10整數表示,最高優先級是10用常量MAX\_PRIORITY表示;最低優先級是1用常量MIN\_PRIORITY;默認優先級是5用常量NORM\_PRIORITY表示。
Thread類提供了setPriority(int newPriority)方法可以設置線程優先級,通過getPriority()方法獲得線程優先級。
**代碼:**

## 23.4.2 等待線程結束
在介紹現在狀態時提到過join()方法,當前主線程調用t1線程的join()方法,則阻塞當前主線程,等待t1線程,如果t1線程結束或等待超時,則當前線程回到就緒狀態。
Thread類提供了多個版本的join(),它們定義如下:
* void join():等待該線程結束。
* void join(long millis):等待該線程結束的時間最長為millis毫秒。如果超時為0意味著要一直等下去。
* void join(long millis, int nanos):等待該線程結束的時間最長為millis毫秒加nanos納秒。
使用join()方法示例代碼如下:
~~~
//HelloWorld.java文件
package com.a51work6;
public class HelloWorld {
//共享變量
static int value = 0; ①
public static void main(String[] args) throws InterruptedException {
System.out.println("主線程 開始...");
// 創建線程t1,參數是一個線程執行對象Runner
Thread t1 = new Thread(() -> { ②
System.out.println("ThreadA 開始...");
for (int i = 0; i < 2; i++) {
System.out.println("ThreadA 執行...");
value++; ③
}
System.out.println("ThreadA 結束...");
}, "ThreadA");
// 開始線程t1
t1.start();
// 主線程被阻塞,等待t1線程結束
t1.join(); ④
System.out.println("value = " + value); ⑤
System.out.println("主線程 結束...");
}
}
~~~
運行結果如下:
~~~
主線程 開始...
ThreadA 開始...
ThreadA 執行...
ThreadA 執行...
ThreadA 結束...
value = 2
主線程 結束...
~~~
上述代碼第①行是聲明了一個共享變量value,這個變量在子線程中修改,然后主線程訪問它。代碼第②行是采用Lambda表達式創建線程,指定線程名為ThreadA。代碼第③行是在子線程ThreadA中修改共享變量value。
代碼第④行是在當前線程(主線程)中調用t1的join()方法,因此會導致主線程阻塞,等待t1線程結束。t1結束后,再執行主線程。代碼第⑤行是打印共享變量value,從運行結果可見value = 2。
如果嘗試將t1.join()語句注釋掉,輸出結果如下,因為此時兩個線程交替運:
~~~
主線程 開始...
value = 0
主線程 結束...
ThreadA 開始...
ThreadA 執行...
ThreadA 執行...
ThreadA 結束...
~~~
> **提示** 使用join()方法的場景是,一個線程依賴于另外一個線程的運行結果,所以調用另一個線程的join()方法等它運行完成。
### 23.4.3 線程讓步
線程類Thread提供靜態方法yield(),調用yield()方法能夠使當前線程給其他線程讓步。它類似于sleep()方法,能夠使運行狀態的線程放棄CPU使用權,暫停片刻,然后重新回到就緒狀態。
與sleep()方法不同的是,sleep()方法是線程進行休眠,能夠給其他線程運行的機會,無論線程優先級高低都有機會運行。而yield()方法只給相同優先級或更高優先級線程機會。
示例代碼如下:
~~~
//Runner.java文件
package com.a51work6;
//線程執行對象
public class Runner implements Runnable {
// 編寫執行線程代碼
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// 打印次數和線程的名字
System.out.printf("第 %d次執行 - %s\n", i,
Thread.currentThread().getName());
Thread.yield(); ①
}
// 線程執行結束
System.out.println("執行完成! " + Thread.currentThread().getName());
}
}
~~~
代碼第①行Thread.yield()能夠使當前線程讓步。
> **提示** yield()方法只能給相同優先級或更高優先級的線程讓步,yield()方法在實際開發中很少使用,
> 因為不可以控制時間,而sleep()方法可以。
### 23.4.4 線程停止
線程體中的run()方法結束,線程進入死亡狀態,線程就停止了。但是有些業務比較復雜,例如想開發一個下載程序,每隔一段執行一次下載任務,下載任務一般會在由子線程執行的,休眠一段時間再執行。這個下載子線程中會有一個死循環,但是為了能夠停止子線程,設置一個結束變量。
示例下面如下:
~~~
//HelloWorld.java文件
package com.a51work6;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class HelloWorld {
private static String command = ""; ①
public static void main(String[] args) {
// 創建線程t1,參數是一個線程執行對象Runner
Thread t1 = new Thread(() -> {
// 一直循環,直到滿足條件在停止線程
while (!command.equalsIgnoreCase("exit")) { ②
// 線程開始工作
// TODO
System.out.println("下載中...");
try {
// 線程休眠
Thread.sleep(10000);
} catch (InterruptedException e) {
}
}
// 線程執行結束
System.out.println("執行完成!");
});
// 開始線程t1
t1.start();
try (InputStreamReader ir = new InputStreamReader(System.in); ③
BufferedReader in = new BufferedReader(ir)) {
// 從鍵盤接收了一個字符串的輸入
command = in.readLine(); ④
} catch (IOException e) {
}
}
}
~~~
上述代碼第①行是設置一個結束變量。代碼第②行是在子線程的線程體中判斷,用戶輸入的是否為exit字符串,如果不是則進行循環,否則結束循環,結束循環就結束了run()方法,線程就停止了。
代碼第③行中的System.in是一個很特殊的輸入流,能夠從控制臺(鍵盤)讀取字符。代碼第④行是通過流System.in讀取鍵盤輸入的字符串。測試是需要注意:在控制臺輸入exit,然后敲Enter鍵,如圖23-6所示。
*****
## 23.5 線程安全
在多線程環境下,訪問相同的資源,有可以會引發線程不安全問題。本節討論引發這些問題的根源和解決方法。
### 23.5.1 臨界資源問題
多一個線程同時運行,有時線程之間需要共享數據,一個線程需要其他線程的數據,否則就不能保證程序運行結果的正確性。
例如有一個航空公司的機票銷售,每一天機票數量是有限的,很多售票點同時銷售這些機票。下面是一個模擬銷售機票系統,示例代碼如下:
~~~
//TicketDB.java文件
package com.a51work6;
//機票數據庫
public class TicketDB {
// 機票的數量
private int ticketCount = 5; ①
// 獲得當前機票數量
public int getTicketCount() { ②
return ticketCount;
}
// 銷售機票
public void sellTicket() { ③
try {
// 等于用戶付款
// 線程休眠,阻塞當前線程,模擬等待用戶付款
Thread.sleep(1000); ④
} catch (InterruptedException e) {
}
System.out.printf("第%d號票,已經售出\n", ticketCount);
ticketCount--; ⑤
}
}
~~~
上述代碼模擬機票銷售過程,代碼第①行是聲明機票數量成員變量ticketCount,這是模擬當天可供銷售的機票數,為了測試方便初始值設置為5。代碼第②行是定義了獲取當前機票數的getTicketCount()方法。代碼第③行是銷售機票方法,售票網點查詢出有沒有票可以銷售,那么會調用sellTicket()方法銷售機票,這個過程中需要等待用戶付款,付款成功后,會將機票數減一,見代碼第⑤行。為模擬等待用戶付款,在代碼第④行使用了sleep()方法讓當前線程阻塞。
調用代碼如下:
~~~
//HelloWorld.java文件
package com.a51work6;
public class HelloWorld {
public static void main(String[] args) {
TicketDB db = new TicketDB();
// 創建線程t1
Thread t1 = new Thread(() -> {
while (true) {
int currTicketCount = db.getTicketCount(); ①
// 查詢是否有票
if (currTicketCount > 0) { ②
db.sellTicket(); ③
} else {
// 無票退出
break;
}
}
});
// 開始線程t1
t1.start();
// 創建線程t2
Thread t2 = new Thread(() -> {
while (true) {
int currTicketCount = db.getTicketCount();
// 查詢是否有票
if (currTicketCount > 0) {
db.sellTicket();
} else {
// 無票退出
break;
}
}
});
// 開始線程t2
t2.start();
}
}
~~~
在HelloWorld中創建了兩個線程,模擬兩個售票網點,沒有線程所做的事情類似。首先獲得當前機票數量(見代碼第①行),然后判斷機票數量是否大于零(見代碼第②行),如果有票則出票(見代碼第②行),否則退出循環,結束線程。
一次運行結果如下:
~~~
第5號票,已經售出
第5號票,已經售出
第3號票,已經售出
第3號票,已經售出
第1號票,已經售出
第0號票,已經售出
~~~
雖然可以能每次運行的結果都不一樣,但是從結果看還是能發現一些問題:同一張票重復銷售了兩次。這些問題的原因是多個線程間共享的數據導致數據的不一致性。
> **提示** 多個線程間共享的數據稱為共享資源或臨界資源,由于是CPU負責線程的調度,程序員無法精確控制多線程的交替順序。這種情況下,多線程對臨界資源的訪問有時會導致數據的不一致性。
## 23.5 線程安全
在多線程環境下,訪問相同的資源,有可以會引發線程不安全問題。
### 23.5.1 臨界資源問題
多個線程同時運行,有時線程之間需要共享數據,一個線程需要其他線程的數據,否則就不能保證程序運行結果的正確性。
例如有一個航空公司的機票銷售,每一天機票數量是有限的,很多售票點同時銷售這些機票。下面是一個模擬銷售機票系統,示例代碼如下:
~~~
//TicketDB.java文件
package com.a51work6;
//機票數據庫
public class TicketDB {
// 機票的數量
private int ticketCount = 5; ①
// 獲得當前機票數量
public int getTicketCount() { ②
return ticketCount;
}
// 銷售機票
public void sellTicket() { ③
try {
// 等于用戶付款
// 線程休眠,阻塞當前線程,模擬等待用戶付款
Thread.sleep(1000); ④
} catch (InterruptedException e) {
}
System.out.printf("第%d號票,已經售出\n", ticketCount);
ticketCount--; ⑤
}
}
~~~
上述代碼模擬機票銷售過程,代碼第①行是聲明機票數量成員變量ticketCount,這是模擬當天可供銷售的機票數,為了測試方便初始值設置為5。代碼第②行是定義了獲取當前機票數的getTicketCount()方法。代碼第③行是銷售機票方法,售票網點查詢出有沒有票可以銷售,那么會調用sellTicket()方法銷售機票,這個過程中需要等待用戶付款,付款成功后,會將機票數減一,見代碼第⑤行。為模擬等待用戶付款,在代碼第④行使用了sleep()方法讓當前線程阻塞。
調用代碼如下:
~~~
//HelloWorld.java文件
package com.a51work6;
public class HelloWorld {
public static void main(String[] args) {
TicketDB db = new TicketDB();
// 創建線程t1
Thread t1 = new Thread(() -> {
while (true) {
int currTicketCount = db.getTicketCount(); ①
// 查詢是否有票
if (currTicketCount > 0) { ②
db.sellTicket(); ③
} else {
// 無票退出
break;
}
}
});
// 開始線程t1
t1.start();
// 創建線程t2
Thread t2 = new Thread(() -> {
while (true) {
int currTicketCount = db.getTicketCount();
// 查詢是否有票
if (currTicketCount > 0) {
db.sellTicket();
} else {
// 無票退出
break;
}
}
});
// 開始線程t2
t2.start();
}
}
~~~
在HelloWorld中創建了兩個線程,模擬兩個售票網點,沒有線程所做的事情類似。首先獲得當前機票數量(見代碼第①行),然后判斷機票數量是否大于零(見代碼第②行),如果有票則出票(見代碼第②行),否則退出循環,結束線程。
一次運行結果如下:
~~~
第5號票,已經售出
第5號票,已經售出
第3號票,已經售出
第3號票,已經售出
第1號票,已經售出
第0號票,已經售出
~~~
雖然可以能每次運行的結果都不一樣,但是從結果看還是能發現一些問題:同一張票重復銷售了兩次。這些問題的根本原因是多個線程間共享的數據導致數據的不一致性。
> **提示** 多個線程間共享的數據稱為共享資源或臨界資源,由于是CPU負責線程的調度,程序員無法精確控制多線程的交替順序。這種情況下,多線程對臨界資源的訪問有時會導致數據的不一致性。
*****
### 23.5.2 多線程同步
為了防止多線程對臨界資源的訪問有時會導致數據的不一致性,Java提供了“互斥”機制,可以**為這些資源對象加上一把“互斥鎖”**,在任一時刻只能由一個線程訪問。
即使該線程出現阻塞,該對象的被鎖定狀態也不會解除,其他線程仍不能訪問該對象,這就多線程同步。
線程同步保證線程安全的重要手段,但是線程同步客觀上會導致性能下降。
**兩種方式實現線程同步**
* 一種是synchronized方法,使用synchronized關鍵字修飾方法,對方法進行同步
* 另一種是synchronized語句,使用synchronized關鍵字放在對象前面限制一段代碼的執行。
**1\. synchronized方法**
synchronized關鍵字修飾方法實現線程同步,方法所在的對象被鎖定,修改23.5.1節售票系統示例, TicketDB.java文件代碼如下:
```
~~~
//TicketDB.java文件
package com.a51work6.method;
//機票數據庫
public class TicketDB {
// 機票的數量
private int ticketCount = 5;
// 獲得當前機票數量
public synchronized int getTicketCount() { ①
return ticketCount;
}
// 銷售機票
public synchronized void sellTicket() { ②
try {
// 等于用戶付款
// 線程休眠,阻塞當前線程,模擬等待用戶付款
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.printf("第%d號票,已經售出\n", ticketCount);
ticketCount--;
}
}
~~~
上述代碼第①行和第②行的方法前都使用了synchronized關鍵字,表明這兩個方法是同步的,被鎖定的,每一個時刻只能由一個線程訪問。并不是每一個方法都有必要加鎖的,要仔細研究加上的必要性,上述代碼第①行加鎖可以防止出現第0號票情況和5張票賣出2次的情況;代碼第②行加鎖是防止出現銷售兩種一樣的票,讀者可以自己測試一下。
采用synchronized方法修改示例,調用代碼HelloWorld.java不需要任何修改。
```
**2. synchronized語句**
synchronized語句方式主要用于第三方類,不方便修改它的代碼情況。同樣是23.5.1節售票系統示例,可以不用修改TicketDB.java類,只修改調用代碼HelloWorld.java實現同步。
```
HelloWorld.java代碼如下:
//HelloWorld.java文件
package com.a51work6.statement;
public class HelloWorld {
public static void main(String[] args) {
TicketDB db = new TicketDB();
// 創建線程t1
Thread t1 = new Thread(() -> {
while (true) {
synchronized (db) { ①
int currTicketCount = db.getTicketCount();
// 查詢是否有票
if (currTicketCount > 0) {
db.sellTicket();
} else {
// 無票退出
break;
}
}
}
});
// 開始線程t1
t1.start();
// 創建線程t2
Thread t2 = new Thread(() -> {
while (true) {
synchronized (db) { ②
int currTicketCount = db.getTicketCount();
// 查詢是否有票
if (currTicketCount > 0) {
db.sellTicket();
} else {
// 無票退出
break;
}
}
}
});
// 開始線程t2
t2.start();
}
}
```
代碼第①行和第②行是使用synchronized語句,將需要同步的代碼用大括號括起來。synchronized后有小括號,將需要同步的對象括起來。
*****
## 23.6 線程間通信
第23.5節的示例只是簡單地為特定對象或方法加鎖,但有時情況會更加復雜,如果兩個線程之間有依賴關系,線程之間必須進行通信,互相協調才能完成工作。
例如有一個經典的堆棧問題,一個線程生成了一些數據,將數據壓棧;另一個線程消費了這些數據,將數據出棧。這兩個線程互相依賴,當堆棧為空時,消費線程無法取出數據時,應該通知生成線程添加數據;當堆棧已滿時,生產線程無法添加數據時,應該通知消費線程取出數據。
為了實現線程間通信,需要使用Object類中聲明的5個方法:
* void wait():使當前線程釋放對象鎖,然后當前線程處于對象等待隊列中阻塞狀態,如圖23-7所示,等待其他線程喚醒。
* void wait(long timeout):同wait()方法,等待timeout毫秒時間。
* void wait(long timeout, int nanos):同wait()方法,等待timeout毫秒加nanos納秒時間。
* void notify():當前線程喚醒此對象等待隊列中的一個線程,如圖23-7所示該線程將進入就緒狀態。
* void notifyAll():當前線程喚醒此對象等待隊列中的所有線程,如圖23-7所示這些線程將進入就緒狀態。

**圖23-7 線程間通信**
> **提示** 圖23-7是圖23-5補充,從圖23-7可見,線程有多種方式進入阻塞狀態,除了通過wait()外還有,加鎖的方式和其他方式,加鎖方式是23.5節介紹的使用synchronized加互斥鎖;其他方式事實上是23.3節線程狀態時介紹的方式,這里不再贅述。
下面看看消費和生產示例中堆棧類代碼:
~~~
//Stack.java文件
package com.a51work6;
//堆棧類
class Stack {
// 堆棧指針初始值為0
private int pointer = 0;
// 堆棧有5個字符的空間
private char[] data = new char[5];
// 壓棧方法,加上互斥鎖
public synchronized void push(char c) { ①
// 堆棧已滿,不能壓棧
while (pointer == data.length) {
try {
// 等待,直到有數據出棧
this.wait();
} catch (InterruptedException e) {
}
}
// 通知其他線程把數據出棧
this.notify();
// 數據壓棧
data[pointer] = c;
// 指針向上移動
pointer++;
}
// 出棧方法,加上互斥鎖
public synchronized char pop() { ②
// 堆棧無數據,不能出棧
while (pointer == 0) {
try {
// 等待其他線程把數據壓棧
this.wait();
} catch (InterruptedException e) {
}
}
// 通知其他線程壓棧
this.notify();
// 指針向下移動
pointer--;
// 數據出棧
return data[pointer];
}
}
~~~
上述代碼實現了同步堆棧類,該堆棧有最多5個元素的空間,代碼第①行聲明了壓棧方法push(),該方法是一個同步方法,在該方法中首先判斷是否堆棧已滿,如果已滿不能壓棧,調用this.wait()讓當前線程進入對象等待狀態中。如果堆棧未滿,程序會往下運行調用this.notify()喚醒對象等待隊列中的一個線程。代碼第②行聲明了出棧方法pop()方法,與push()方法類似,這里不再贅述。
調用代碼如下:
```
//HelloWorld.java文件
package com.a51work6;
public class HelloWorld {
public static void main(String args[]) {
Stack stack = new Stack(); ①
// 下面的消費者和生產者所操作的是同一個堆棧對象stack
// 生產者線程
Thread producer = new Thread(() -> { ②
char c;
for (int i = 0; i < 10; i++) {
// 隨機產生10個字符
c = (char) (Math.random() * 26 + 'A');
// 把字符壓棧
stack.push(c);
// 打印字符
System.out.println("生產: " + c);
try {
// 每產生一個字符線程就睡眠
Thread.sleep((int) (Math.random() * 1000));
} catch (InterruptedException e) {
}
}
});
// 消費者線程
Thread consumer = new Thread(() -> { ③
char c;
for (int i = 0; i < 10; i++) {
// 從堆棧中讀取字符
c = stack.pop();
// 打印字符
System.out.println("消費: " + c);
try {
// 每讀取一個字符線程就睡眠
Thread.sleep((int) (Math.random() * 1000));
} catch (InterruptedException e) {
}
}
});
producer.start(); // 啟動生產者線程
consumer.start(); // 啟動消費者線程
}
}
```
上述代碼第①行創建堆棧對象。代碼第②行創建生產者線程,代碼第③行創建消費者線程。