JAVA編程講義之J多線程
前面我們進行的都是單線程編程,即一個程序只有一個從頭到尾的執行路徑。這樣做的優點是易于編程,無需考慮過多的情況。但是,由于單線程需要在上一個任務完成之后才開始下一個任務,所以其效率比較低。在真實的項目運行過程中都具有多任務同時執行的特點,比如項目中用到的數據庫連接池,這些任務在執行的時候互不干擾,這就需要多線程技術。
14.1 線程概述
隨著計算機技術的飛速發展,計算機的操作系統一般都是支持多任務的,即在同一個時間內執行多個程序,一般的操作系統都需要引入多進程與多線程技術。
14.1.1 進程
在學習線程之前,需要先簡單了解一下什么是進程。進程是程序的一次執行過程,是系統運行程序的基本單位。在操作系統中,每個獨立執行的程序都可以稱之為是一個進程,包括創建、運行、消亡3個階段。在操作系統中,進程是獨立存在的,它擁有自己獨立的資源,多個進程可以在同一個處理器上并發執行且互不影響。
例如,每一臺計算機都可以同時運行騰訊QQ以及QQ音樂兩個程序,在聽音樂時聊天。此時,同時按下“Ctrl+Alt+Del”打開Windows任務管理器,在進程選項卡中就可以查看進程,如圖14.2所示。圖14.2中,可以看到騰訊QQ、QQ音樂以及此時電腦正在運行的其他程序,將軟件正常關閉或者右鍵結束進程,都可以使這個進程消亡。
需要明確指出的的是,表面上看操作系統中是多個進程同時執行的,如圖14.2中所示的騰訊QQ、QQ音樂以及其他程序都在同時執行,但實際上這些進程并不是同時運行。因為,計算機中所有的程序都是由CPU執行的,且一般的計算機都只有一個CPU,而一個CPU只能同時執行一個進程,但是操作系統會給各個同時打開的程序分配占用時間,在這段時間里可以執行QQ聊天,當這段時間段過了則切換到QQ音樂,之后在切換到其他程序。由于CPU的執行速度很快,人們根本發覺不到它是在切換執行,所以會有一種計算機同時執行多個程序的感覺。
知識點撥:中央處理器(central processing unit,簡稱CPU)作為計算機系統的運算和控制核心,是信息處理、程序運行的最終執行單元。它是一塊超大規模的集成電路,它的功能主要是解釋計算機指令以及處理計算機軟件中的數據。CPU的能力高低直接影響了整個電腦的運行速度。
14.1.2 線程
通過前面關于進程的講解我們可以知道,每個程序都是一個進程。但是,現在流行的操作系統不但支持多進程,還支持多線程,在一個進程中還可以有多個執行單元同時執行,這些執行單元就稱為線程。換句話說,操作系統可以同時執行多個任務,每個任務就是一個進程,每個進程又可以同時執行多個子任務,每個子任務就是一個線程。例如,圖14.1所示的計算機運行狀態中,騰訊QQ就是一個進程,然而我們在聊天的時候可以同時打開多個聊天窗口,并且互不影響,這就是多個線程同時運行。打開Windows任務管理器,點擊性能選項卡,可以查看當前系統的線程數,如圖14.2所示。圖14.2顯示,當前系統的總進程數為213、總線程數為2773,總線程數要比總進程數多很多,原因就是一個進程里面可以有多個線程在同時執行。
所謂多線程,指的就是在一個進程中多個線程可以同時存在、同時運行、互不影響。
當有多個線程在操作時,如果系統只有一個CPU,則它根本不可能真正同時進行一個以上的線程,它只能把CPU運行時間劃分成若干個時間段,再將時間段分配給各個線程執行,在一個時間段的線程代碼運行時,其它線程處于掛起狀,這種方式稱為并發(Concurrent)。并發環境是以“掛起→執行→掛起”的方式將很小的時間片分給各線程,給用戶一種線程在同時運行的錯覺。在并發環境中,多線程縮短了系統的響應時間,給用戶更好的體驗。
圖14.1 軟件進程 圖14.2 當前系統線程
進程和線程一樣都是實現并發機制的一種手段,進程是可以獨立運行的一段程序,線程是比進程更小的執行單位。一個線程只能屬于一個進程,一個進程可以擁有多個線程。線程一般不擁有自己的系統資源,但是可以訪問其隸屬的進程的資源。如圖14.3所示,給出了進程與線程的關系結構。
圖14.3 進程與線程的關系結構
Java語言對多線程提供直接支持,通過其設定的機制組織代碼,可以將按照邏輯順序執行的代碼片段轉成并發執行,而每一個代碼片段還是一個邏輯上比較完整的程序代碼段。
14.2 多線程的實現
Java語言提供了3種實現多線程的方式:繼承Thread類實現多線程、實現Runnable接口實現多線程、使用Callable接口和Future接口實現多線程。
14.2.1 繼承Thread類實現多線程
Java提供了Thread類,代表線程,它位于java.lang包中,開發人員可以通過繼承Thread類來創建并啟動多線程,具體步驟如下:
? 從Thread類派生出一個子類,并且在子類中重寫run()方法。
? 用這個子類創建一個實例對象。
? 調用對象的start()方法啟動線程。
啟動一個新線程時,需要創建一個Thread類的實例, Thread類的常用構造方法如表14.1所示。
表14.1 Thread類常用構造法
構造方法聲明
方法描述
public Thread()
創建新的Thread對象,自動生成的線程名稱為 "Thread-"+n,其中n為整數
public Thread(String name)
創建新的Thread對象,name是新線程的名稱
public Thread(Runnable target)
創建新的Thread對象,其中target是run()方法被調用時的對象
public Thread(Runnable target, String name)
創建新的Thread對象,其中target是run()方法被調用時的對象,name是新線程的名字
表14.1中列出了Thread類中的常用構造方法,創建線程實例的時候需要使用這些構造方法,線程中真正的功能代碼寫在這個類的run()方法中。當一個類繼承Thread類之后,要重寫父類的run()方法。另外,Thread類還有一些常用方法,如表14.2所示。
表14.2 Thread類常用方法
常用方法聲明
方法描述
String getName()
返回該線程的名稱
Thread.State getState()
返回該線程的狀態
boolean isAlive()
判斷該線程是不是處于活躍狀態
void setName(String name)
更改線程的名字,使其與參數的name保持一致
void start()
開始執行線程,Java 虛擬機調用該線程里面的 run() 方法
static void sleep(long millis)
在指定的毫秒數內讓當前正在執行的線程休眠(暫停執行),此操作受到系統計時器和調度程序精度和準確性的影響
static Thread currentThread()
返回當前正在運行的線程的對象的引用
接下來,通過案例來演示使用繼承Thread類的方式創建多線程,如例14-1所示。
例14-1 Demo1401.java
1 package com.aaa.p140201;
2
3 public class Demo1401 {
4 public static void main(String[] args) {
5 MyThread myThread1 = new MyThread(); // 創建MyThread實例對象
6 MyThread myThread2 = new MyThread();
7 myThread1.start(); // 開啟線程
8 myThread2.start();
9 }
10 }
11 class MyThread extends Thread {
12 public void run() { // 重寫run()方法
13 for (int i = 0; i < 10; i++) {
14 if (i % 2 != 0) {
15 System.out.println(Thread.
16 currentThread().getName() + ":" + i);
17 }
18 }
19 }
20 }
程序的運行結果如下:
Thread-0:1
Thread-1:1
Thread-0:3
Thread-1:3
Thread-0:5
Thread-0:7
Thread-0:9
Thread-1:5
Thread-1:7
Thread-1:9
例14-1中,聲明了一個類MyThread類,繼承Thread類,并且在類中重寫了run()方法,方法的功能是循環打印小于10的奇數,其中currentThread()方法是Thread類的靜態方法,調用該方法返回的是當前正在執行的線程對象的引用。Demo1401類在main()方法中創建了兩個MyThread類的實例對象,分別調用實例對象的start()方法啟動兩個線程,兩個線程都運行成功。以上就是繼承Thread類創建多線程的方式。
注意:如果start()方法調用一個已經啟動的線程,程序會報IllegalThreadStateException異常。
14.2.2 實現Runnable接口實現多線程
Runnable是Java中用于實現線程的接口,從理論上來講,任何實現線程功能的類都必須實現該接口。第14.2.1節講到的繼承Thread類的方式創建多線程,實際上就是因為Thread類實現了Runnable接口,所以它的子類才具有了線程的功能。但是,Java只支持單繼承,一個類只能有一個父類,當一個類繼承Thread類之后就不能再繼承其他類,因此可以用實現Runnable接口的方式創建多線程,這種創建線程的方式更具有靈活性,同時可令用戶線程能夠具有其他類的一些特性,所以這種方法是經常使用的。通過實現Runnable接口創建并啟動多線程的步驟如下:
? 定義Runnable接口實現類,并重寫run()方法。
? 創建Runnable接口實現類的實例對象,并將該實例對象傳遞給Thread類的一個構造方法,該實例對象提供線程體run()方法。
? 調用實例對象的start()方法啟動線程。
接下來,通過案例來演示如何通過實現Runnable接口的方式創建多線程,如例14-2所示。
例14-2 Demo1402.java
1 package com.aaa.p140202;
2
3 public class Demo1402 {
4 public static void main(String[] args) {
5 MyThread myThread = new MyThread(); // 創建myThread實例
6 //第1個參數是myThread對象,第2個參數是線程名稱
7 new Thread(myThread, "線程1").start();// 啟動線程
8 new Thread(myThread, "線程2").start();
9 }
10 }
11 class MyThread implements Runnable {
12 public void run() { // 重寫run()方法
13 for (int i = 0; i < 10; i++) {
14 if (i % 2 != 0) {
15 System.out.println(Thread.
16 currentThread().getName() + ":" + i);
17 }
18 }
19 }
20 }
程序的運行結果如下:
線程1:1
線程1:3
線程1:5
線程1:7
線程1:9
線程2:1
線程2:3
線程2:5
線程2:7
線程2:9
例14-2中,MyThread類實現了Runnable接口并且重寫了run()方法,方法的功能是循環打印小于10的奇數。Demo1402類在main()方法中以MyThread類的實例分別創建并開啟兩個線程對象,調用public Thread(Runnable target, String name)構造方法的目的是指定線程的名稱“線程1”和“線程2”。以上就是通過實現Runnable接口的方式創建多線程。
14.2.3 通過Callable接口和Future接口實現多線程
前文講解了創建多線程的兩種方式,但是這兩種方式都有一個缺陷,在執行完任務之后無法獲取線程的執行結果,如果想要獲取執行結果,就必須通過共享變量或者使用線程通信的方式來達到效果,這樣使用起來就比較麻煩。于是,JDK5.0后Java便提供了Callable接口來解決這個問題,接口內有一個call()方法,這個方法是線程執行體,有返回值且可以拋出異常。通過實現Callable接口創建并啟動多線程的步驟如下:
? 定義Callable接口實現類,指定返回值的類型,并重寫call()方法。
? 創建Callable實現類的實例。
? 使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了Callable對象的call()方法的返回值。
? 將FutureTask類的實例注冊進入Thread中并啟動線程。
? 采用FutureTask
Callable接口不是Runnable接口的子接口,所以不能直接作為Thread類構造方法的參數,而且call()方法有返回值,是被調用者。JDK5.0中提供了Future接口,該接口有一個FutureTask實現類,該類實現了Runnable接口,封裝了Callable對象的call()方法的返回值,所以該類可以作為參數傳入Thread類中。接下來先了解一下Future接口的方法,如表14.3所示。
表14.3 Future接口的方法
接口方法聲明
方法描述
boolean cancel(boolean b)
試圖取消對該任務的執行
V get()
如有必要,等待計算完成,然后獲取其結果
V get(long timeout, TimeUnit unit)
如有必要,最多等待使計算完成所用時間之后,獲取其結果(若結果可用)
boolean isCancelled()
如果在任務正常完成前將其取消,則返回 true
boolean isDone()
如果任務已完成,則返回 true
接下來,通過案例來演示如何通過Callable接口和Future接口創建多線程,如例14-3所示。
例14-3 Demo1403.java
1 package com.aaa.p140203;
2 import java.util.concurrent.Callable;
3 import java.util.concurrent.FutureTask;
4
5 public class Demo1403 {
6 public static void main(String[] args) {
7 Callable
8 // 使用FutureTask來包裝Callable對象
9 FutureTask
10 for (int i = 0; i < 15; i++) {
11 System.out.println(Thread.currentThread().getName() + ":" + i);
12 if (i == 1) {
13 // FutureTask對象作為Thread對象的參數創建新的線程
14 Thread thread = new Thread(futureTask);
15 thread.start(); // 啟動線程
16 }
17 }
18 System.out.println("主線程循環執行完畢..");
19 try {
20 // 取得新創建線程中的call()方法返回值
21 String result = futureTask.get();
22 System.out.println("result = " + result);
23 } catch (Exception e) {
24 e.printStackTrace();
25 }
26 }
27 }
28 class MyThread implements Callable
29 public String call() {
30 for (int i = 10; i > 0; i--) {
31 System.out.println(Thread.currentThread().getName() + "倒計時:" + i);
32 }
33 return "線程執行完畢!!!";
34 }
35 }
程序的第1次運行結果如下:
main:0
main:1
main:2
main:3
main:4
main:5
main:6
main:7
main:8
main:9
main:10
main:11
main:12
main:13
main:14
主線程循環執行完畢..
Thread-0倒計時:10
Thread-0倒計時:9
Thread-0倒計時:8
Thread-0倒計時:7
Thread-0倒計時:6
Thread-0倒計時:5
Thread-0倒計時:4
Thread-0倒計時:3
Thread-0倒計時:2
Thread-0倒計時:1
result = 線程執行完畢!!!
程序的第2次運行結果如下:
main:0
main:1
main:2
main:3
main:4
main:5
main:6
main:7
main:8
main:9
Thread-0倒計時:10
main:10
Thread-0倒計時:9
main:11
Thread-0倒計時:8
main:12
Thread-0倒計時:7
main:13
main:14
主線程循環執行完畢..
Thread-0倒計時:6
Thread-0倒計時:5
Thread-0倒計時:4
Thread-0倒計時:3
Thread-0倒計時:2
Thread-0倒計時:1
result = 線程執行完畢!!!
例14-3中,MyThread類實現了Callable接口,指定了返回值的類型并且重寫了call()方法。該方法主要是用于打印倒計時的時間。main()方法中執行15次循環,并且在循環的過程中啟動子線程并獲取子線程的返回值。
反復執行例14-3的程序,會發現有一個規律:“result = 線程執行完畢!!!”一直都是在最后輸出,而“主線程循環執行完畢..”輸出的位置則不固定,有時候會在子線程循環前,有時候會在子線程循環后,有時候也會在子線程循環中。之所以會出現這種現象,是因為通過get()方法獲取子線程的返回值時,子線程的方法沒有執行完畢,所以get()方法就會阻塞,當子線程中的call()方法執行完畢,get()方法才能取到返回值。以上就是使用Callable接口和Future接口的方式創建多線程。
14.2.4 三種實現多線程方式的對比分析
前面講解了創建多線程的3種方式,這3種方式各有優缺點,具體如表14.4所示。
表14.4 三種實現多線程方式的對比
實現方式
優劣
具體內容
繼承Thread類創建多線程
優點
程序代碼簡單
使用run()方法可以直接調用線程的其他方法
缺點
只能繼承Thread類
不能實現資源共享
實現Runnable接口創建多線程
優點
符合面向對象的設計思想
便于繼承其他的類
能實現資源共享
缺點
編程比較復雜
使用Callable接口和Future接口創建多線程
優點
便于繼承其他的類
有返回值,可以拋異常
缺點
編程比較復雜
表14.4列出了3種創建多線程方式的優點和缺點,想要代碼簡潔就采用第1種方式,想要實現資源共享就采用第2種方式,想要有返回值并且能拋異常就采用第3種方式。
14.2.5 后臺線程
Java中有一種線程,它是在后臺運行的,它的主要任務就是為其他線程提供服務,這種線程被稱為后臺線程或守護線程。JVM的垃圾回收機制使用的就是后臺線程。
后臺線程有一個重要的特征:如果所有的前臺線程都死亡,后臺線程會自動死亡。
調用Thread類的setDaemon(true)方法可以將指定的線程設置為后臺線程,所有的前臺線程都死亡的時候,后臺線程就會自動死亡。Thread類還提供了一個isDaemon()方法,該方法主要是用于判斷一個線程是否是一個后臺線程,
接下來,通過案例來演示后臺線程的使用,如例14-4所示。
例14-4 Demo1404.java
1 package com.aaa.p140205;
2
3 public class Demo1404 {
4 public static void main(String[] args) {
5 // 創建MyThread類實例
6 System.out.println("青年學子梁山伯辭家求學,路上偶遇女扮男裝的學子祝英臺,");
7 System.out.println("兩人一見如故,志趣相投,遂于草橋結拜為兄弟。");
8 MyThread1 myThread1 = new MyThread1("梁山伯:");
9 myThread1.start();// 開啟線程
10 MyThread2 myThread2 = new MyThread2("祝英臺:");
11 myThread2.setDaemon(true);
12 myThread2.start();// 開啟線程
13 }
14 }
15 class MyThread1 extends Thread {
16 private String socialStatus;
17 public MyThread1(String socialStatus) {
18 this.socialStatus = socialStatus;
19 }
20 @Override
21 public void run() {
22 for (int i = 1; i <= 20; i++) {
23 System.out.println(socialStatus + i);
24 }
25 }
26 }
27 class MyThread2 extends Thread {
28 private String socialStatus;
29 public MyThread2(String socialStatus) {
30 this.socialStatus = socialStatus;
31 }
32 @Override
33 public void run() {
34 for (int i = 1; i <= 100; i++) {
35 System.out.println(socialStatus + i);
36 }
37 }
38 }
程序的運行結果如下:
青年學子梁山伯辭家求學,路上偶遇女扮男裝的學子祝英臺,
兩人一見如故,志趣相投,遂于草橋結拜為兄弟。
梁山伯:1
梁山伯:2
祝英臺:1
梁山伯:3
梁山伯:4
梁山伯:5
梁山伯:6
梁山伯:7
梁山伯:8
梁山伯:9
梁山伯:10
祝英臺:2
梁山伯:11
祝英臺:3
梁山伯:12
梁山伯:13
梁山伯:14
梁山伯:15
祝英臺:4
梁山伯:16
祝英臺:5
梁山伯:17
祝英臺:6
祝英臺:7
祝英臺:8
祝英臺:9
祝英臺:10
梁山伯:18
梁山伯:19
祝英臺:11
梁山伯:20
祝英臺:12
祝英臺:13
祝英臺:14
祝英臺:15
祝英臺:16
祝英臺:17
祝英臺:18
祝英臺:19
例14-4中,MyThread1與MyThread2類繼承了Thread類并且實現了run()方法,MyThread1中的run()方法調用20次循環,MyThread2中的run()方法調用100次循環,Demo1404類在main()方法中分別創建MyThread1與MyThread2的實例,MyThread2類中調用setDaemon(true),此時該線程被設置為后臺線程。開啟前臺線程和后臺線程就發現MyThread2線程本應該執行循環100次,但是結果發現執行19次就結束了,這是因為前臺線程執行完畢后,線程死亡,只剩下后臺線程,當線程只剩下后臺線程的時候程序就沒有執行的必要了,所以后臺線程也會隨之退出。這就是后臺線程的基本使用。
注意:setDaemon(true)必須在start()方法之前調用,否則會引發異常。
14.3 線程的生命周期
在講解了線程的創建及使用之后,下面再來講解一下線程的生命周期。在Java中,任何對象都有生命周期,線程也不例外。線程有新建(New)、就緒(Runnable)、運行(Running)、阻塞(Blocked)和死亡(Terminated)5種狀態,從新建到死亡稱之為線程的生命周期,如圖14.4所示。
14.3.1 新建狀態和就緒狀態
當程序使用new關鍵字創建一個線程后,該線程處于新建狀態,此時JVM給它分配一塊內存,但不可運行。
當線程對象調用了start()方法之后,該線程處于就緒狀態,JVM會為它創建方法調用棧和程序計數器。處于就緒狀態的線程并沒有開始運行,只是表示該線程可以運行了。獲得CPU的使用權之后線程即可開始運行。
注意:啟動線程使用的是start()方法,而不是run()方法!如果直接調用run()方法,系統會把當前的線程識別為一個普通的對象,而run()方法也就是一個普通的方法,并不是線程的執行體。
接下來,通過案例來演示線程的啟動,如例14-5所示。
例14-5 Demo1405.java
1 package p140301;
2
3 public class Demo1405 {
4 public static void main(String[] args) throws InterruptedException {
5 new MyThread().run();
6 new MyThread().run();
7 }
8 }
9 class MyThread extends Thread {
10 @Override
11 public void run() {
12 for (int i = 0; i < 5; i++) {
13 System.out.println(Thread.currentThread().getName()+" "+i);
14 }
15 }
16 }
程序的運行結果如下:
main 0
main 1
main 2
main 3
main 4
main 0
main 1
main 2
main 3
main 4
線程創建之后如果直接調用run()方法,程序的運行結果是整個程序只有一個線程——主線程。通過上面的程序運行結果不難看出,啟動線程的正確方法是調用線程的start()方法,而不是直接調用run()方法,否則就會變成單線程。
14.3.2 運行狀態和阻塞狀態
運行狀態是指處于就緒狀態的線程占用了CPU,執行程序代碼。并發執行時,如果CPU的占用時間超時,則會執行其他線程。只有處于就緒狀態的線程才可以轉換到運行狀態。
阻塞狀態是指線程因為一些原因放棄CPU使用權,暫時停止運行。當線程處于阻塞狀態時,Java虛擬機不會給線程分配CPU,直到線程重新進入就緒狀態,它才有機會轉換到運行狀態。
下面列舉一下線程由運行狀態轉換成阻塞狀態的原因,以及如何從阻塞狀態轉換成就緒狀態:
? 當線程調用了某個對象的suspend()方法時,也會使線程進入阻塞狀態,如果想進入就緒狀態需要使用resume()方法喚醒該線程。
? 當線程試圖獲取某個對象的同步鎖時,如果該鎖被其他線程持有,則當前線程就會進入阻塞狀態,如果想從阻塞狀態進入就緒狀態必須獲取到其他線程持有的鎖。關于鎖的概念,會在14.5.2節詳細講解。
? 當線程調用了Thread類的sleep()方法時,也會使線程進入阻塞狀態,在這種情況下,需要等到線程睡眠的時間結束,線程會自動進入就緒狀態。關于線程休眠的概念,會在第14.4.2節詳細講解。
? 當線程調用了某個對象的wait()方法時,也會使線程進入阻塞狀態,如果想進入就緒狀態,需要使用notify()方法或notifyAll()方法喚醒該線程。關于wait()會在14.5.4節詳細講解。
? 當在一個線程中調用了另一個線程的join()方法時,會使當前線程進入阻塞狀態,在這種情況下,要等到新加入的線程運行結束才會結束阻塞狀態,進入就緒狀態。調用join()方法,意味著線程插隊,關于線程插隊的概念,會在第14.4.4節詳細講解。
注意:線程從阻塞狀態只能進入就緒狀態,不能直接進入運行狀態。
14.3.3 死亡狀態
線程會以如下方式結束,結束之后線程就處于死亡狀態:
? 線程的run()方法正常執行完畢,線程正常結束。
? 線程拋出異常(Exception)或錯誤(Error)導致線程死亡。
? 調用線程對象的stop()方法結束線程。
線程一旦轉換為死亡狀態,就不能運行且不能轉換為其他狀態。
注意:不要對處于死亡狀態的線程調用start()方法,程序只能對新建狀態的線程調用start()方法。判斷線程是否死亡可以使用線程的isAlive()方法,當線程處于就緒、運行、阻塞這3種狀態時,放方法返回true。當線程處于新建和死亡狀態時,該方法返回false。
14.4 線程的調度
通過前面的學習我們知道,線程就緒之后就可以運行,但這并不意味著這個線程能夠立刻運行,如果想讓線程運行就必須獲得CPU的使用權。因為多線程是并發運行的,所以在一臺只有一個CPU的計算機上就必須考慮CPU是如何分配的。線程的調度就是為線程分配CPU使用權的,常用如下兩種模型:
? 分時調度模型:讓所有的線程輪流獲得CPU的使用權,平均分配每個線程占用CPU的時間。
? 搶占式調度模型:優先讓可運行池中優先級高的線程占用CPU,若運行池中線程優先級相同,則遵循“先進先出”的原則。
本節就來詳細講解線程調度的相關知識。
14.4.1 線程的優先級
所有處于就緒狀態的線程會根據他們的優先級存放在可運行池中,優先級高的線程運行的機會比較多,優先級低的線程運行機會比較少。Thread類的setPriority(int newPriority)方法用于設置線程的優先級,getPriority()方法用于獲取線程的優先級。優先級可以用Thread類中的靜態常量來表示,如表14.5所示。
表14.5 Thread類的靜態常量
常量聲明
方法描述
static int MAX_PRIORITY
取值為10,表示最高優先級。
static int NORM_PRIORITY
取值為5,表示默認優先級。
static int MIN_PRIORITY
取值為1,表示最低優先級。
表14.4中列出了Thread類中與優先級有關的3個靜態常量,在設置線程的優先級的時候可以使用這些靜態常量。
接下來,通過案例來演示線程優先級的使用,如例14-6所示。
例14-6 Demo1406.java
1 package com.aaa.p140401;
2
3 public class Demo1406 {
4 public static void main(String[] args) throws InterruptedException {
5 // 創建MyThread實例
6 System.out.println("吃飯時吃菜的順序:");
7 MyThread myThread1 = new MyThread("水煮肉片");
8 MyThread myThread2 = new MyThread("醬燜茼蒿");
9 MyThread myThread3 = new MyThread("樹根炒樹皮");
10 myThread1.setPriority(Thread.MIN_PRIORITY); // 設置優先級
11 myThread2.setPriority(Thread.MAX_PRIORITY);
12 myThread3.setPriority(Thread.NORM_PRIORITY);
13 myThread1.start(); // 開啟線程
14 myThread2.start();
15 myThread3.start();
16 }
17 }
18
19 class MyThread extends Thread {
20 private final String Cuisine;
21
22 public MyThread(String Cuisine) {
23 this.Cuisine = Cuisine;
24 }
25
26 @Override
27 public void run() {
28 for (int i = 0; i < 5; i++) {
29 System.out.println(Cuisine + i);
30 }
31 }
32 }
程序的運行結果如下:
吃飯時吃菜的順序:
醬燜茼蒿0
樹根炒樹皮0
醬燜茼蒿1
醬燜茼蒿2
醬燜茼蒿3
醬燜茼蒿4
樹根炒樹皮1
樹根炒樹皮2
樹根炒樹皮3
樹根炒樹皮4
水煮肉片0
水煮肉片1
水煮肉片2
水煮肉片3
水煮肉片4
例14-6中,聲明了MyThread類,繼承Thread類并在類中重寫了run()方法,run()方法內循環打印結果。Demo1406類在main()方法中先創建了3個MyThread類的實例并指定線程的名稱,再使用setPriority(int newPriority)方法設置線程的優先級,最后調用start()方法啟動線程,從執行結果來看,優先級高的會優先執行。但是需要注意的是,優先級比較低的不一定永遠最后執行,也有可能先執行,只不過機率稍微小一點。
注意:Thread類的setPriority(int newPriority)方法可以設置10種優先級,但是優先級的級別需要操作系統的支持,不同的操作系統上支持的優先級也各不同,所以要盡量避免直接用數字指定線程優先級,應該使用Thread類的3個靜態常量指定線程優先級別,這樣可以保證程序有很好的可移植性。
14.4.2 線程休眠sleep()
線程的調度是按照線程的優先級的高低順序搶占CPU資源的,優先級高的線程會優先搶占CPU資源,線程不執行完,優先級低的線程就無法搶占CPU資源。Thread類提供了sleep()方法,該方法可使正在執行的線程進入阻塞狀態,也叫線程休眠,休眠時間內該線程是不運行的,休眠時間結束后線程才繼續運行。如果想讓優先級低的線程搶占CPU資源,就需要調用sleep()方法,該方法是人為地控制線程,讓正在執行的線程暫停一段固定的時間,在暫停的時間內,線程讓出CPU資源,讓優先級低的線程有機會運行。休眠方法結束之后,線程將進入可運行狀態。
sleep()方法有兩種形式,具體如下:
static void sleep(long millis)
static void sleep(long millis, int nanos)
上述兩種形式,第1種中的參數指的是線程休眠的毫秒數,第2種的參數指的是線程休眠的毫秒數和納秒數。使用sleep(long millis)方法時會報InterruptedException異常,此時必須要捕獲異常或拋出異常。
接下來,通過案例來演示線程休眠,如例14-7所示。
例7-%2 Demo1407.java
1 package com.aaa.p140402;
2 import java.text.SimpleDateFormat;
3 import java.util.Date;
4
5 public class Demo1407 {
6 public static void main(String[] args) throws InterruptedException {
7 // 創建MyThread實例
8 System.out.println("吃飯時吃菜的順序");
9 MyThread myThread1 = new MyThread("水煮肉片");
10 MyThread myThread2 = new MyThread("醬燜茼蒿");
11 MyThread myThread3 = new MyThread("樹根炒樹皮");
12 myThread1.setPriority(Thread.MIN_PRIORITY); // 設置優先級
13 myThread2.setPriority(Thread.MAX_PRIORITY);
14 myThread3.setPriority(Thread.NORM_PRIORITY);
15 myThread3.start(); // 開啟線程
16 Thread.sleep(2000);
17 myThread2.start();
18 Thread.sleep(2000);
19 myThread1.start();
20 }
21 }
22
23 class MyThread extends Thread {
24 private final String Cuisine;
25
26 public MyThread(String Cuisine) {
27 this.Cuisine = Cuisine;
28 }
29
30 @Override
31 public void run() {
32 for (int i = 0; i < 5; i++) {
33 System.out.println(Cuisine + i);
34 }
35 }
36 }
程序的運行結果如下:
吃飯時吃菜的順序
樹根炒樹皮0
樹根炒樹皮1
樹根炒樹皮2
樹根炒樹皮3
樹根炒樹皮4
醬燜茼蒿0
醬燜茼蒿1
醬燜茼蒿2
醬燜茼蒿3
醬燜茼蒿4
水煮肉片0
水煮肉片1
水煮肉片2
水煮肉片3
水煮肉片4
例14-7中,線程啟動后調用Thread類的sleep()方法,讓程序休眠2秒,打印的結果跟例14-6對比可以看到有很明顯的差別。
14.4.3 線程讓步yield()
Thread類還提供一個yield()方法,該方法和sleep()方法類似,它也可以讓當前正在執行的線程暫停,sleep()方法在暫停期間對象鎖不釋放從而導致線程阻塞,而yield()方法只是將線程的狀態轉化為就緒狀態,等待線程調度器的再次調用,線程調度器有可能會將剛才處于就緒狀態的線程重新調度出來,這就是線程讓步。
接下來,通過案例來演示線程讓步。如例14-8所示。
例14-8 Demo1408.java
1 package com.aaa.p140403;
2
3 public class Demo1408 {
4 public static void main(String[] args) throws InterruptedException {
5 // 創建MyThread實例
6 System.out.println("吃飯時吃菜的順序:");
7 MyThread myThread1 = new MyThread("水煮肉片");
8 MyThread myThread2 = new MyThread("醬燜茼蒿");
9 myThread1.setPriority(Thread.MAX_PRIORITY); // 設置優先級
10 myThread2.setPriority(Thread.MIN_PRIORITY);
11 myThread1.start();
12 myThread2.start();
13 }
14 }
15
16 class MyThread extends Thread {
17 private final String Cuisine;
18
19 public MyThread(String Cuisine) {
20 this.Cuisine = Cuisine;
21 }
22
23 @Override
24 public void run() {
25 for (int i = 0; i < 5; i++) {
26 Thread.yield(); // 設置線程讓步
27 System.out.println(Cuisine + i);
28 }
29 }
30 }
程序的運行結果如下:
吃飯時吃菜的順序:
水煮肉片0
醬燜茼蒿0
水煮肉片1
醬燜茼蒿1
水煮肉片2
醬燜茼蒿2
醬燜茼蒿3
醬燜茼蒿4
水煮肉片3
水煮肉片4
例14-8中,聲明MyThread類,繼承Thread類,并實現了run()方法,方法內循環打印數字0~4,每次打印前調用yield()方法線程讓步。Demo1408類在main()方法中創建MyThread類實例,分別創建并開啟兩個線程。這里注意,并不是線程執行到yield()方法就一定切換到其他線程,也有可能線程繼續執行。
注意:調用yield()方法之后,可以使具有與當前線程相同優先級的線程有運行的機會。如果有其他的線程與當前的線程具有相同的優先級并且處于可運行狀態,該方法會把調用yield()方法的線程放入可運行池中,并允許其他線程運行。如果沒有同等的優先級的線程使可運行狀態,yield()方法什么也不做,即該線程講繼續運行。
14.4.4 線程插隊join()
Thread類提供了join()方法,當某個線程執行中調用其他線程的join()方法時,線程被阻塞,直到join()方法所調用的線程結束,這種情況稱為線程插隊。
接下來,通過案例來演示線程插隊,如例14-9所示。
例14-9 Demo1409.java
1 package com.aaa.p140404;
2
3 public class Demo1409 {
4 public static void main(String[] args) throws Exception {
5 // 創建MyThread實例
6 System.out.println("吃飯時吃菜的順序:");
7 MyThread myThread1 = new MyThread("醬燜茼蒿");
8 myThread1.start();// 開啟線程
9 for (int i = 1; i < 6; i++) {
10 if (i == 3) {
11 try {
12 System.out.println("醬燜茼蒿要開始插隊了...");
13 myThread1.join();// 線程插入
14 } catch (Exception e) {
15 e.printStackTrace();
16 }
17 }
18 System.out.println("水煮肉片" + i);
19 }
20 }
21 }
22 class MyThread extends Thread {
23 private String socialStatus;
24 private int tickets = 10;
25 public MyThread(String socialStatus) {
26 this.socialStatus = socialStatus;
27 }
28 @Override
29 public void run() {
30 for (int i = 1; i < 6; i++) {
31 System.out.println(socialStatus + i);
32 }
33 }
34 }
程序的運行結果如下:
吃飯時吃菜的順序:
水煮肉片1
水煮肉片2
醬燜茼蒿要開始插隊了...
醬燜茼蒿1
醬燜茼蒿2
醬燜茼蒿3
醬燜茼蒿4
醬燜茼蒿5
水煮肉片3
水煮肉片4
水煮肉片5
例14-9中,聲明了MyThread類,繼承Thread類并在類中實現了run()方法,方法內循環打印“醬燜茼蒿”。Demo1409類在main()方法中創建MyThread類實例并啟動線程,main()方法中也循環打印吃菜的順序,當變量i的值為3時,調用join()方法插入子線程后子線程開始執行,直到子線程執行完,main()方法的主線程才能繼續執行。
14.5 多線程同步
前面講解了線程的基本使用,多線程可以提高程序的運行效率,但是多線程也會導致很多不合理的現象的出現,比如在賣外賣的時候出現超賣的現象。之所以出現這些現象,是因為系統的調度具有隨機性,多線程在操作同一數據時,很容易出現這種錯誤。接下來我們來講解一下如何解決這種錯誤。
14.5.1 線程安全
關于線程安全,我們通過賣外賣來展示。賣外賣的基本流程大致為:首先,要知道一共有多少外賣,每賣掉1份外賣,對應的數量就會減1;其次,可以有多個窗口賣外賣,當外賣的數量為0時就停止售賣。如果是單線程,這個流程不會出現什么問題,但是如果這個流程放在多線程并發的情況下,就會出現超賣的情況。
接下來,我們通過案例來演示這個問題。如例14-10所示。
例14-10 Demo1410.java
1 package com.aaa.p140501;
2
3 public class Demo1410 {
4 public static void main(String[] args) {
5 Takeout takeout = new Takeout();
6 Thread t1 = new Thread(takeout);
7 Thread t2 = new Thread(takeout);
8 Thread t3 = new Thread(takeout);
9 t1.start();
10 t2.start();
11 t3.start();
12 }
13 }
14 class Takeout implements Runnable {
15 private int takeout = 5;
16
17 public void run() {
18 for (int i = 0; i < 100; i++) {
19 if (takeout> 0) {
20 try {
21 Thread.sleep(100);
22 } catch (InterruptedException e) {
23 e.printStackTrace();
24 }
25 System.out.println(
26 "賣出第" + takeout+ "份外賣,還剩" + --takeout + "份外賣");
27 }
28 }
29 }
30 }
程序的運行結果如下:
賣出第5份外賣,還剩4份外賣
賣出第5份外賣,還剩3份外賣
賣出第3份外賣,還剩2份外賣
賣出第2份外賣,還剩1份外賣
賣出第1份外賣,還剩0份外賣
賣出第0份外賣,還剩-1份外賣
例14-10中,聲明了Takeout類,實現了Runnable接口。首先,在類中定義一個int類型的變量takeout,這個變量代表的是外賣的總數量;然后,重寫run()方法,run()方法中循環賣外賣每賣1份外賣,外賣總數減1,為了演示可能出現的問題,通過調用sleep()的方法讓程序在每次循環的時候休眠100毫秒;最后,Demo1410類在main()方法中創建并啟動3個線程,模擬3個窗口同時賣外賣。運行結果可以看出,第5五份外賣重復賣了2次,剩余的外賣還出現了-1份。
例14-10中之所以會出現超賣的情況,是因為run()方法的循環中判斷外賣總數量是否大于0,如果大于0就會繼續售賣,但售賣的時候線程調用了sleep()方法導致程序每次循環都會休眠100毫秒,這就會出現,1個線程執行到此處休眠的同時,第2和第3個線程也進入執行,所以賣出的數量就會變多,這就是線程安全的問題。
14.5.2 多線程中的同步代碼塊
我們使用多個線程訪問同一資源的時候,若多個線程只有讀操作,那么不會發生線程安全問題,但是如果多個線程中對資源有讀和寫的操作,就容易出現線程安全問題。前面賣外賣的案例中就出現了線程安全的問題。為了解決這種問題,我們可以使用線程鎖。
線程鎖主要是給方法或代碼塊加鎖。當某個方法或者代碼塊使用鎖時,那么在同一時刻至多僅有一個線程在執行該段代碼。當有多個線程訪問同一對象的加鎖方法或代碼塊時,同一時間只有一個線程在執行,其余線程必須要等待當前線程執行完之后才能執行該代碼段。但是,其余線程可以訪問該對象中的非加鎖代碼塊。
Java的多線程引入了同步代碼塊,當多個線程使用同一個共享資源時,可以將處理共享資源的代碼放置在一個使用synchronized關鍵字來修飾的代碼塊中。具體示例如下:
synchronized (obj) {
… ????????????????????// 要同步的代碼塊
}
Java中每個對象都有一個內置鎖。當程序運行到synchronized同步代碼塊時,就會獲得當前執行的代碼塊里面的對象鎖。一個對象只有一個鎖稱為鎖對象。如果一個線程獲得該鎖,其他線程就無法再次獲得這個對象的鎖,直到第一個線程釋放鎖。釋放鎖是指持線程退出了synchronized同步方法或代碼塊。
如上所示,synchronized(obj)中的obj就是同步鎖,它是同步代碼塊的關鍵,當線程執行同步代碼塊時,會先檢查同步監視器的標志位,默認情況下標志位為1。標志位為1的時候線程會執行同步代碼塊,同時將標志位改為0;當第2個線程執行同步代碼塊前,先檢查標志位,如果檢查到標志位為0,第2個線程就會進入阻塞狀態;當第1個線程執行完同步代碼塊內的代碼時,標志位重新改為1,第2個線程進入同步代碼塊。
接下來,通過修改例14-10的代碼來演示如何使用同步代碼塊解決線程安全問題,如例14-11所示。
例14-11 Demo1411.java
1 package com.aaa.p140502;
2
3 public class Demo1411 {
4 public static void main(String[] args) {
5 Takeout takeout = new Takeout();
6 Thread t1 = new Thread(takeout);
7 Thread t2 = new Thread(takeout);
8 Thread t3 = new Thread(takeout);
9 t1.start();
10 t2.start();
11 t3.start();
12 }
13 }
14 class Takeout implements Runnable {
15 private int takeout = 5;
16
17 public void run() {
18 for (int i = 0; i < 100; i++) {
19 synchronized (this) { // this代表當前對象
20 if (takeout > 0) {
21 try {
22 Thread.sleep(100);
23 } catch (InterruptedException e) {
24 e.printStackTrace();
25 }
26 System.out.println("賣出第" + takeout + "份外賣,還剩" +
27 --takeout + "份外賣");
28 }
29 }
30 }
31 }
32 }
程序的運行結果如下:
賣出第5份外賣,還剩4份外賣
賣出第4份外賣,還剩3份外賣
賣出第3份外賣,還剩2份外賣
賣出第2份外賣,還剩1份外賣
賣出第1份外賣,還剩0份外賣
例14-11與前邊的例14-10幾乎是完全一樣,區別就是例14-11在run()方法的循環中執行售賣操作時,將操作變量takeout的操作都放到同步代碼塊中,在使用同步代碼塊時必須指定一個需要同步的對象,一般使用當前對象(this)即可。將例14-10修改為例14-11后,多次運行該程序同樣不會出現重復的售賣或超賣的情況。
注意:同步代碼塊中的鎖對象可以是任意類型的對象,但多個線程共享的鎖對象必須是相同的。“任意”說的是共享鎖對象的類型。所以,鎖對象的創建代碼不能放到run()方法中,否則每個線程運行到run()方法都會創建一個新對象,這樣每個線程都會有一個不同的鎖,每個鎖都有自己的標志位,線程之間便無法產生同步的效果。
14.5.3 synchronized修飾的同步方法
第14.5.3節講解了使用同步代碼塊解決線程安全問題,另外Java還提供了同步方法,即用synchronized關鍵字修飾的方法,它的監視器是調用該方法的對象,使用同步方法同樣可以解決線程安全的問題。
接下來,通過修改例14-10的代碼來演示如何使用同步方法解決線程安全問題,如例14-12所示。
例14-12 Demo1412.java
1 package com.aaa.p140503;
2
3 public class Demo1412 {
4 public static void main(String[] args) throws Exception {
5 Takeout takeout = new Takeout();
6 Thread t1 = new Thread(takeout);
7 Thread t2 = new Thread(takeout);
8 Thread t3 = new Thread(takeout);
9 t1.start();
10 t2.start();
11 t3.start();
12 }
13 }
14
15 class Takeout implements Runnable {
16 private int takeout = 5;
17
18 public synchronized void run() {
19 for (int i = 0; i < 100; i++) {
20 if (takeout > 0) {
21 try {
22 Thread.sleep(100);
23 } catch (InterruptedException e) {
24 e.printStackTrace();
25 }
26 System.out.println(
27 "賣出第" + takeout + "份外賣,還剩" + --takeout + "份外賣");
28 }
29 }
30 }
31 }
程序的運行結果如下:
賣出第5份外賣,還剩4份外賣
賣出第4份外賣,還剩3份外賣
賣出第3份外賣,還剩2份外賣
賣出第2份外賣,還剩1份外賣
賣出第1份外賣,還剩0份外賣
例14-12與前邊的例14-10幾乎一樣,區別就是例14-10的run()方法沒有使用synchronized關鍵字修飾,將例14-10修改為例14-12后,多次運行程序不會出現超賣或者重復售賣的情況。
注意:同步方法的鎖就是調用該方法的對象,也就是this所指向的對象,但是靜態方法不需要創建對象就可以用“類名.方法名()”的方式進行調用,這時的鎖則不再是this,靜態同步方法的鎖是該方法所在類的class對象,該對象可以直接用“類名.class”的方式獲取。
14.5.4 生產者和消費者
不同的線程執行不同的任務,有些復雜的程序需要多個線程共同完成一個任務,這個時候就需要線程之間能夠相互通信。線程通信中的一個經典問題就是生產者和消費者問題。java.lang包中Object類中提供了三種方法用于線程的通信。如表14.6所示
表14.6 Thread類的靜態常量
方法
方法描述
void wait()
導致當前線程等待,直到另一個線程調用該對象的 notify() 方法或 notifyAll() 方法。
void notify ()
喚醒正在等待對象監視器的單個線程。
void notifyAll ()
喚醒正在等待對象監視器的所有線程。
表14.6列舉了線程通信需要使用的三個方法,這三個方法只有在synchronized方法或synchronized代碼塊中才能使用,否則會報IllegalMonitorStateException異常。
生產者和消費者問題(Producer-consumer problem),也稱有限緩沖問題(Bounded-buffer problem),是一個多線程同步問題的經典案例。該問題描述了兩個共享固定大小緩沖區的線程(即所謂的“生產者”和“消費者”)在實際運行時會發生的問題。生產者的主要作用是生成一定量的數據放到緩沖區中,然后重復此過程。與此同時,消費者也在緩沖區消耗這些數據。如圖14.5所示。
生產者和消費者問題會導致死鎖的出現,下面簡單介紹一下死鎖。
死鎖是指兩個或兩個以上的進程在執行過程中,由于競爭資源或者由于彼此通信而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。此時稱系統處于死鎖狀態或系統產生了死鎖。
生產者和消費者問題如果不加以協調可能會出現以下情況:緩沖區中數據已滿,而生產者依然占用著它,消費者等著生產者讓出空間從而去消費產品,生產者等著消費者消費產品,從而向空間中添加產品。互相等待,從而發生死鎖。
接下來,通過一個案例來演示如何解決生產者和消費者問題,如例14-13所示。
例14-13 Demo1413.java
1 package com.aaa.p140504;
2
3 import java.util.LinkedList;
4 public class Demo1413 {
5 private static final int MAX_NUM = 5; // 設置倉庫的最大值
6 private LinkedList
7 class Producer implements Runnable{ // 生產者
8 @Override
9 public void run() {
10 while(true){
11 try{
12 Thread.sleep(1000);
13 synchronized (list) {
14 while (list.size() + 1 > MAX_NUM) {
15 System.out.println("生產者:" +
16 Thread.currentThread().getName() + " 倉庫已滿");
17 try {
18 list.wait();
19 } catch (InterruptedException e) {
20 e.printStackTrace();
21 }
22 }
23 list.add(new Object());
24 System.out.println("生產者:" + Thread.currentThread().
25 getName() + " 生產了一個產品,現庫存量:" + list.size());
26 list.notifyAll();
27 }
28 }catch (InterruptedException e){
29 e.printStackTrace();
30 }
31 }
32 }
33 }
34 class Consumer implements Runnable{ // 消費者
35 @Override
36 public void run() {
37 while(true){
38 try{
39 Thread.sleep(3000);
40 synchronized (list) {
41 while (list.size() == 0) {
42 System.out.println("消費者:" + Thread.currentThread().
43 getName() + " 倉庫為空");
44 try {
45 list.wait();
46 } catch (InterruptedException e) {
47 e.printStackTrace();
48 }
49 }
50 list.remove();
51 System.out.println("消費者:" + Thread.currentThread().
52 getName() + " 消費一個產品,現庫存量:" + list.size());
53 list.notifyAll();
54 }
55 }catch (InterruptedException e){
56 e.printStackTrace();
57 }
58 }
59 }
60 }
61 public static void main(String[] args) {
62 Demo1413 proAndCon = new Demo1413();
63 Producer producer = proAndCon.new Producer();
64 Consumer consumer = proAndCon.new Consumer();
65 // 開啟3個生產者線程和3個消費者線程
66 for (int i = 0; i < 3; i++) {
67 Thread pro = new Thread(producer);
68 pro.start();
69 Thread con = new Thread(consumer);
70 con.start();
71 }
72 }
73 }
程序的運行結果如下:
生產者:Thread-2 生產了一個產品,現庫存量:1
生產者:Thread-0 生產了一個產品,現庫存量:2
生產者:Thread-4 生產了一個產品,現庫存量:3
生產者:Thread-2 生產了一個產品,現庫存量:4
生產者:Thread-0 生產了一個產品,現庫存量:5
生產者:Thread-4 倉庫已滿
消費者:Thread-3 消費一個產品,現庫存量:4
生產者:Thread-4 生產了一個產品,現庫存量:5
生產者:Thread-2 倉庫已滿
消費者:Thread-1 消費一個產品,現庫存量:4
生產者:Thread-0 生產了一個產品,現庫存量:5
消費者:Thread-5 消費一個產品,現庫存量:4
生產者:Thread-2 生產了一個產品,現庫存量:5
生產者:Thread-4 倉庫已滿
生產者:Thread-2 倉庫已滿
生產者:Thread-0 倉庫已滿
消費者:Thread-3 消費一個產品,現庫存量:4
生產者:Thread-0 生產了一個產品,現庫存量:5
生產者:Thread-2 倉庫已滿
生產者:Thread-4 倉庫已滿
。。。。。。。。。。。。。。
例14-12中,使用wait()和notify()的方法來解決生產者和消費者的問題。對于生產者而言,如果緩存區的容量大于設定的最大容量,程序就會調用wait()方法來阻塞線程。否則,就會向緩存區中添加對象,然后調用notifyAll()方法來喚醒其他被阻塞的線程。對于消費者而言,如果緩存區中沒有對象,程序會調用wait()方法阻塞線程,否則就移除緩沖區的對象,并調用notifyAll()方法來喚醒其他被阻塞的線程。
生產者線程運行1次休眠1s,消費者線程運行一次休眠3s。例14-13中有3個生產者和3個消費者,也就是我們說的多對多的情況。倉庫的容量為5,可以看出消費的速度明顯慢于生產的速度,從而避免了死鎖的出現。
14.6 本章小結
? 進程是程序的一次執行過程,是操作系統運行程序的基本單位。在一個進程中還可以有多個執行單元同時執行,這些執行單元就稱為線程。
? 多線程的實現方式有3種:繼承Thread類、實現Runnable接口和調用Callable接口和Future接口。繼承Thread類的方式代碼簡單,實現Runnable的方式能夠實現資源共享,調用Callable接口和Future接口的方式能夠拋出異常并且有返回值。
? 設置后臺線程的時候需要調用Thread類的setDaemon(boolean on)方法,當主線程結束的時候后臺線程也會退出。
? 線程的生命周期包括新建、就緒、運行、阻塞和死亡5種狀態。當程序使用new關鍵字創建一個線程后,該線程處于新建狀態;當線程對象調用了start()方法之后,該線程處于就緒狀態;當線程搶占了CPU之后,就處于運行狀態;當線程放棄了CPU 的使用權的時候,該線程處于阻塞狀態;當線程結束的時候,就處于死亡狀態。
? 線程設置優先級的時候,調用的是Thread類的setPriority(int newPriority)方法,優先級越高的線程將會獲得更多的執行機會。
? 線程休眠的時候,調用的是Thread類的靜態方法sleep(),當線程調用sleep()方法之后會進入阻塞狀態。在休眠的時候,該線程不會獲得任何執行的機會,sleep()方法通常用于暫停程序的執行。
? yield()方法是讓當前線程暫停,并轉化為就緒狀態等待線程調度器的再次調用。
? 當某個線程在執行中調用其他線程的join()方法時,當前線程將被阻塞,直到被join()方法加入的線程執行完為止。
? 線程的安全問題可以通過線程的同步代碼塊和同步方法來實現。
? 線程間通信的經典問題就是生產者和消費者問題。java.lang.Object類中提供了wait()、notify()和notifyAll() 3種方法用于線程間的通信。在線程通信的時候應避免死鎖問題的出現。
14.7 理論習題與實踐練習
1.填空題
1.1 ________是Java程序的并發機制,它能同步共享數據、處理不同的事件。
1.2 線程有新建、就緒、運行、________和死亡五種狀態。
1.3 線程的創建有三種方法:________、________、和________。
2.選擇題
2.1 線程調用了sleep()方法后,該線程將進入( )狀態。
A.可運行狀態?????????????????????????B.運行狀態
C.阻塞狀態????????????????????????????D.終止狀態
2.2 關于Java線程,下面說法錯誤的是( )。
A.線程是以CPU為主體的行為????????B.Java利用線程使整個系統成為異步
C.繼承Thread類可以創建線程????????????D.新線程被創建后,它將自動開始運行
2.3 線程控制方法中,yield()的作用是( )。
A.返回當前線程的引用????????????????B.使比其低的優先級線程執行
C.強行終止線程????????????????????????D.只讓給同優先級線程運行
2.4 當( )方法終止時,能使線程進入死亡狀態。
A.run()????????????????????????????????B.setPrority()
C.yield()????????????????????????????D.sleep()
2.5 線程通過( )方法可以改變優先級。
A.run()????????????????????????????????B.setPrority()
C.yield()????????????????????????????D.sleep()
3.思考題
3.1 請簡述什么是線程?什么是進程?
3.2 請簡述Java有哪幾種創建線程的方式?
3.3 請簡述什么是線程的生命周期?
3.4 請簡述啟動一個線程是用什么方法?
4.編程題
4.1 利用多線程設計一個程序,同時輸出100以內的奇數和偶數,以及當前運行的線程名稱,輸出數字完畢后輸出end。
4.2 編寫一個繼承Thread類的方式實現多線程的程序。該類MyThread有兩個屬性,一個字符串WhoAmI代表線程名,一個整數delay代表該線程隨機要休眠的時間。利用有參的構造函數指定線程名稱和休眠時間,休眠時間為隨機數,線程執行時,顯示線程名和要休眠時間。最后,在main()方法中創建3個線程對象以展示執行情況。
Java 任務調度 多線程
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。