1. 程式人生 > 實用技巧 >【JAVA基礎】多執行緒

【JAVA基礎】多執行緒

第十八章 多執行緒 18.1 基本概念

18.1.1 程式和程序的概念

程式 - 資料結構 + 演算法,主要指存放在硬碟上的可執行檔案。
程序 - 主要指執行在記憶體中的可執行檔案。 目前主流的作業系統都支援多程序,為了讓作業系統同時可以執行多個任務,但程序是重量級的, 也就是新建一個程序會消耗CPU和記憶體空間等系統資源,因此程序的數量比較侷限。

18.1.2 執行緒的概念

為了解決上述問題就提出執行緒的概念,執行緒就是程序內部的程式流,也就是說作業系統內部支援多 程序的,而每個程序的內部又是支援多執行緒的,執行緒是輕量的,新建執行緒會共享所在程序的系統資 源,因此目前主流的開發都是採用多執行緒。 多執行緒是採用時間片輪轉法來保證多個執行緒的併發執行,所謂併發就是指巨集觀並行微觀序列的機 制。

18.2 執行緒的建立(重中之重) 18.2.1 Thread類的概念

java.lang.Thread類代表執行緒,任何執行緒物件都是Thread類(子類)的例項。 Thread類是執行緒的模板,封裝了複雜的執行緒開啟等操作,封裝了作業系統的差異性。

18.2.2 建立方式

自定義類繼承Thread類並重寫run方法,然後建立該類的物件呼叫start方法。 自定義類實現Runnable介面並重寫run方法,建立該類的物件作為實參來構造Thread型別的對 象,然後使用Thread型別的物件呼叫start方法。

18.2.3 相關的方法

Thread()

Thread(Runnable target)

使用無參的方式構造物件

根據引數指定的引用來構造物件,其中Runnable是個介面類
型

Thread(Runnable target, String name)

根據引數指定引用和名稱來構造物件

void run()

若使用Runnable引用構造了執行緒物件,呼叫該方法時最終調 用介面中的版本 若沒有使用Runnable引用構造執行緒物件,呼叫該方法時則啥 也不做

void start() 用於啟動執行緒,Java虛擬機器會自動呼叫該執行緒的run方法

18.2.4 執行流程

執行main方法的執行緒叫做主執行緒,執行run方法的執行緒叫做新執行緒/子執行緒。 main方法是程式的入口,對於start方法之前的程式碼來說,由主執行緒執行一次,當start方法呼叫成 功後執行緒的個數由1個變成了2個,新啟動的執行緒去執行run方法的程式碼,主執行緒繼續向下執行,兩 個執行緒各自獨立執行互不影響。 當run方法執行完畢後子執行緒結束,當main方法執行完畢後主執行緒結束。 兩個執行緒執行沒有明確的先後執行次序,由作業系統排程演算法來決定。

18.2.5 方式的比較

繼承Thread類的方式程式碼簡單,但是若該類繼承Thread類後則無法繼承其它類,而實現 Runnable介面的方式程式碼複雜,但不影響該類繼承其它類以及實現其它介面,因此以後的開發中 推薦使用第二種方式。

18.2.6 匿名內部類的方式 使用匿名內部類的方式來建立和啟動執行緒。

18.3 執行緒的生命週期(熟悉)

方法宣告 功能介紹

新建狀態 - 使用new關鍵字建立之後進入的狀態,此時執行緒並沒有開始執行。
就緒狀態 - 呼叫start方法後進入的狀態,此時執行緒還是沒有開始執行。
執行狀態 - 使用執行緒排程器呼叫該執行緒後進入的狀態,此時執行緒開始執行,當執行緒的時間片執行完 畢後任務沒有完成時回到就緒狀態。
消亡狀態 - 當執行緒的任務執行完成後進入的狀態,此時執行緒已經終止。
阻塞狀態 - 當執行緒執行的過程中發生了阻塞事件進入的狀態,如:sleep方法。 阻塞狀態解除後進入就緒狀態。

18.4 執行緒的編號和名稱(熟悉)

long getId()

void setName(String name)

案例題目

獲取呼叫物件所表示執行緒的編號

設定/修改執行緒的名稱為引數指定的數值
自定義類繼承Thread類並重寫run方法,在run方法中先列印當前執行緒的編號和名稱,然後將執行緒
的名稱修改為"zhangfei"後再次列印編號和名稱。
要求在main方法中也要列印主執行緒的編號和名稱。

18.5 常用的方法(重點)

static void yield()

int getPriority()

void join()

boolean isDaemon()

案例題目

當前執行緒讓出處理器(離開Running狀態),使當前執行緒進入Runnable 狀態等待

獲取執行緒的優先順序

等待該執行緒終止

用於判斷是否為守護執行緒

static void sleep(times)

使當前執行緒從 Running 放棄處理器進入Block狀態, 休眠times毫秒, 再返 回到Runnable如果其他執行緒打斷當前執行緒的Block(sleep), 就會發生 InterruptedException。

void setPriority(int newPriority)

修改執行緒的優先順序。 優先順序越高的執行緒不一定先執行,但該執行緒獲取到時間片的機會會更多 一些

void setDaemon(boolean on)

用於設定執行緒為守護執行緒

程式設計建立兩個執行緒,執行緒一負責列印1 ~ 100之間的所有奇數,其中執行緒二負責列印1 ~ 100之間的 所有偶數。
在main方法啟動上述兩個執行緒同時執行,主執行緒等待兩個執行緒終止。

18.6 執行緒同步機制(重點) 18.6.1 基本概念

當多個執行緒同時訪問同一種共享資源時,可能會造成資料的覆蓋等不一致性問題,此時就需要對線 程之間進行通訊和協調,該機制就叫做執行緒的同步機制。 多個執行緒併發讀寫同一個臨界資源時會發生執行緒併發安全問題。 非同步操作:多執行緒併發的操作,各自獨立執行。

同步操作:多執行緒序列的操作,先後執行的順序。 18.6.2 解決方案

由程式結果可知:當兩個執行緒同時對同一個賬戶進行取款時,導致最終的賬戶餘額不合理。 引發原因:執行緒一執行取款時還沒來得及將取款後的餘額寫入後臺,執行緒二就已經開始取款。 解決方案:讓執行緒一執行完畢取款操作後,再讓執行緒二執行即可,將執行緒的併發操作改為序列操 作。

經驗分享:在以後的開發儘量減少序列操作的範圍,從而提高效率。 18.6.3 實現方式

在Java語言中使用synchronized關鍵字來實現同步/物件鎖機制從而保證執行緒執行的原子性,具體 方式如下:

方法宣告 功能介紹

使用同步程式碼塊的方式實現部分程式碼的鎖定,格式如下: synchronized(類型別的引用) {

     編寫所有需要鎖定的程式碼;

}

使用同步方法的方式實現所有程式碼的鎖定。 直接使用synchronized關鍵字來修飾整個方法即可 該方式等價於:

synchronized(this) { 整個方法體的程式碼 } 18.6.4 靜態方法的鎖定

當我們對一個靜態方法加鎖,如:
public synchronized static void xxx(){....}

那麼該方法鎖的物件是類物件。每個類都有唯一的一個類物件。獲取類物件的方式:類名.class。 靜態方法與非靜態方法同時使用了synchronized後它們之間是非互斥關係的。 原因在於:靜態方法鎖的是類物件而非靜態方法鎖的是當前方法所屬物件。

18.6.5 注意事項 使用synchronized保證執行緒同步應當注意:

多個需要同步的執行緒在訪問同步塊時,看到的應該是同一個鎖物件引用。 在使用同步塊時應當儘量減少同步範圍以提高併發的執行效率。

18.6.6 執行緒安全類和不安全類

StringBuffer類是執行緒安全的類,但StringBuilder類不是執行緒安全的類。
Vector類和 Hashtable類是執行緒安全的類,但ArrayList類和HashMap類不是執行緒安全的類。 Collections.synchronizedList() 和 Collections.synchronizedMap()等方法實現安全。

18.6.7 死鎖的概念

執行緒一執行的程式碼:

public void run(){
synchronized(a){ //持有物件鎖a,等待物件鎖b

synchronized(b){ 編寫鎖定的程式碼;

} }

}

執行緒二執行的程式碼:

public void run(){
synchronized(b){ //持有物件鎖b,等待物件鎖a

synchronized(a){ 編寫鎖定的程式碼;

} }

}

注意: 在以後的開發中儘量減少同步的資源,減少同步程式碼塊的巢狀結構的使用!

18.6.8 使用Lock(鎖)實現執行緒同步 (1)基本概念

從Java5開始提供了更強大的執行緒同步機制—使用顯式定義的同步鎖物件來實現。 java.util.concurrent.locks.Lock介面是控制多個執行緒對共享資源進行訪問的工具。 該介面的主要實現類是ReentrantLock類,該類擁有與synchronized相同的併發性,在以後的執行緒 安全控制中,經常使用ReentrantLock類顯式加鎖和釋放鎖。

(2)常用的方法

ReentrantLock() 使用無參方式構造物件 獲取鎖
void unlock() 釋放鎖

(3)與synchronized方式的比較

Lock是顯式鎖,需要手動實現開啟和關閉操作,而synchronized是隱式鎖,執行鎖定程式碼後自動 釋放。
Lock只有同步程式碼塊方式的鎖,而synchronized有同步程式碼塊方式和同步方法兩種鎖。 使用Lock鎖方式時,Java虛擬機器將花費較少的時間來排程執行緒,因此效能更好。

18.6.9 Object類常用的方法

void wait()

void notify()

功能介紹

用於使得執行緒進入等待狀態,直到其它執行緒呼叫notify()或notifyAll()方 法

用於喚醒等待的單個執行緒

void wait(long 用於進入等待狀態,直到其它執行緒呼叫方法或引數指定的毫秒數已經過 timeout) 去為止

18.6.10 執行緒池(熟悉) (1)實現Callable介面

從Java5開始新增加建立執行緒的第三種方式為實現java.util.concurrent.Callable介面。 常用的方法如下:

V call() 計算結果並返回 (2)FutureTask類

java.util.concurrent.FutureTask類用於描述可取消的非同步計算,該類提供了Future介面的基本實 現,包括啟動和取消計算、查詢計算是否完成以及檢索計算結果的方法,也可以用於獲取方法呼叫 後的返回結果。

常用的方法如下:

FutureTask(Callable callable) 根據引數指定的引用來建立一個未來任務 獲取方法計算的結果

(3)執行緒池的由來

在伺服器程式設計模型的原理,每一個客戶端連線用一個單獨的執行緒為之服務,當與客戶端的會話結束 時,執行緒也就結束了,即每來一個客戶端連線,伺服器端就要建立一個新執行緒。 如果訪問伺服器的客戶端很多,那麼伺服器要不斷地建立和銷燬執行緒,這將嚴重影響伺服器的性 能。

(4)概念和原理

執行緒池的概念:首先建立一些執行緒,它們的集合稱為執行緒池,當伺服器接受到一個客戶請求後,就 從執行緒池中取出一個空閒的執行緒為之服務,服務完後不關閉該執行緒,而是將該執行緒還回到執行緒池 中。 線上程池的程式設計模式下,任務是提交給整個執行緒池,而不是直接交給某個執行緒,執行緒池在拿到任務 後,它就在內部找有無空閒的執行緒,再把任務交給內部某個空閒的執行緒,任務是提交給整個執行緒 池,一個執行緒同時只能執行一個任務,但可以同時向一個執行緒池提交多個任務。

(5)相關類和方法 從Java5開始提供了執行緒池的相關類和介面:java.util.concurrent.Executors類和

java.util.concurrent.ExecutorService介面。 其中Executors是個工具類和執行緒池的工廠類,可以建立並返回不同型別的執行緒池,常用方法如

下:

static ExecutorService newCachedThreadPool()

建立一個可根據需要建立新執行緒的 執行緒池

static ExecutorService newFixedThreadPool(int 建立一個可重用固定執行緒數的執行緒 nThreads) 池

static ExecutorService newSingleThreadExecutor() 建立一個只有一個執行緒的執行緒池 其中ExecutorService介面是真正的執行緒池介面,主要實現類是ThreadPoolExecutor,常用方法

如下:

void execute(Runnable command)

void shutdown()

執行任務和命令,通常用於執行Runnable Callable 啟動有序關閉

方法宣告 功能介紹