Java並髮指南1:並發基礎與Java多執行緒

  • 2019 年 11 月 20 日
  • 筆記

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫里查看

本文是微信公眾號【Java技術江湖】的《Java並髮指南》其中一篇,本文大部分內容來源於網路,為了把本文主題講得清晰透徹,也整合了很多我認為不錯的技術部落格內容,引用其中了一些比較好的部落格文章,如有侵權,請聯繫作者。

該系列博文會告訴你如何全面深入地學習Java並發技術,從Java多執行緒基礎,再到並發編程的基礎知識,從Java並發包的入門和實戰,再到JUC的源碼剖析,一步步地學習Java並發編程,並上手進行實戰,以便讓你更完整地了解整個Java並發編程知識體系,形成自己的知識框架。

為了更好地總結和檢驗你的學習成果,本系列文章也會提供一些對應的面試題以及參考答案。

如果對本系列文章有什麼建議,或者是有什麼疑問的話,也可以關注公眾號【Java技術江湖】聯繫作者,歡迎你參與本系列博文的創作和修訂。

1多執行緒的優點

  • 資源利用率更好
  • 程式設計在某些情況下更簡單
  • 程式響應更快

1.1資源利用率更好案例

方式1 從磁碟讀取一個文件需要5秒,處理一個文件需要2秒。處理兩個文件則需要14秒

1    5秒讀取文件A2    2秒處理文件A3    5秒讀取文件B4    2秒處理文件B5    ---------------------6    總共需要14秒  

方式2 從磁碟中讀取文件的時候,大部分的CPU非常的空閑。它可以做一些別的事情。通過改變操作的順序,就能夠更好的使用CPU資源。看下面的順序:

1    5秒讀取文件A2    5秒讀取文件B + 2秒處理文件A3    2秒處理文件B4    ---------------------5    總共需要12秒  

總結:多執行緒並發效率提高2秒

1.2程式響應更快

設想一個伺服器應用,它在某一個埠監聽進來的請求。當一個請求到來時,它把請求傳遞給工作者執行緒(worker thread),然後立刻返回去監聽。而工作者執行緒則能夠處理這個請求並發送一個回復給客戶端。

while(server is active){          listenThread for request         hand request to workerThread      }

這種方式,服務端執行緒迅速地返回去監聽。因此,更多的客戶端能夠發送請求給服務端。這個服務也變得響應更快。

2多執行緒的代價

2.1設計更複雜

多執行緒一般都複雜。在多執行緒訪問共享數據的時候,這部分程式碼需要特別的注意。執行緒之間的交互往往非常複雜。不正確的執行緒同步產生的錯誤非常難以被發現,並且重現以修復。

2.2上下文切換的開銷

上下文切換當CPU從執行一個執行緒切換到執行另外一個執行緒的時候,它需要先存儲當前執行緒的本地的數據,程式指針等,然後載入另一個執行緒的本地數據,程式指針等,最後才開始執行。

CPU會在一個上下文中執行一個執行緒,然後切換到另外一個上下文中執行另外一個執行緒。

上下文切換並不廉價。如果沒有必要,應該減少上下文切換的發生。

2.3增加資源消耗

每個執行緒需要消耗的資源:

CPU,記憶體(維持它本地的堆棧),作業系統資源(管理執行緒)

3競態條件與臨界區

當多個執行緒競爭同一資源時,如果對資源的訪問順序敏感,就稱存在競態條件。導致競態條件發生的程式碼區稱作臨界區。

多執行緒同時執行下面的程式碼可能會出錯:

public class Counter {  	protected long count = 0;    	public void add(long value) {  		this.count = this.count + value;  	}  }

想像下執行緒A和B同時執行同一個Counter對象的add()方法,我們無法知道作業系統何時會在兩個執行緒之間切換。JVM並不是將這段程式碼視為單條指令來執行的,而是按照下面的順序

從記憶體獲取 this.count 的值放到暫存器  將暫存器中的值增加value  將暫存器中的值寫回記憶體        觀察執行緒A和B交錯執行會發生什麼    	this.count = 0;     A:	讀取 this.count 到一個暫存器 (0)     B:	讀取 this.count 到一個暫存器 (0)     B: 	將暫存器的值加2     B:	回寫暫存器值(2)到記憶體. this.count 現在等於 2     A:	將暫存器的值加3

由於兩個執行緒是交叉執行的,兩個執行緒從記憶體中讀出的初始值都是0。然後各自加了2和3,並分別寫回記憶體。最終的值可能並不是期望的5,而是最後寫回記憶體的那個執行緒的值,上面例子中最後寫回記憶體可能是執行緒A,也可能是執行緒B

4執行緒的運行與創建

Java 創建執行緒對象有兩種方法:

  • 繼承 Thread 類創建執行緒對象
  • 實現 Runnable 介面類創建執行緒對象

注意:

在java中,每次程式運行至少啟動2個執行緒。一個是main執行緒,一個是垃圾收集執行緒。因為每當使用java命令執行一個類的時候,實際上都會啟動一個jvm,每一個jvm實際上就是在作業系統中啟動了一個進程。

5執行緒的狀態和優先順序

執行緒優先順序1 到 10 ,其中 1 是最低優先順序,10 是最高優先順序。

狀態

  • new(新建)
  • runnnable(可運行)
  • blocked(阻塞)
  • waiting(等待)
  • time waiting (定時等待)
  • terminated(終止)

狀態轉換

執行緒狀態流程如下:

  • 執行緒創建後,進入 new 狀態
  • 調用 start 或者 run 方法,進入 runnable 狀態
  • JVM 按照執行緒優先順序及時間分片等執行 runnable 狀態的執行緒。開始執行時,進入 running 狀態
  • 如果執行緒執行 sleep、wait、join,或者進入 IO 阻塞等。進入 wait 或者 blocked 狀態
  • 執行緒執行完畢後,執行緒被執行緒隊列移除。最後為 terminated 狀態

程式碼

public class MyThreadInfo extends Thread {    	@Override // 可以省略  	public void run() {  		System.out.println("run");  		// System.exit(1);  	}    	public static void main(String[] args) {  		MyThreadInfo thread = new MyThreadInfo();  		thread.start();    		System.out.println("執行緒唯一標識符:" + thread.getId());  		System.out.println("執行緒名稱:" + thread.getName());  		System.out.println("執行緒狀態:" + thread.getState());  		System.out.println("執行緒優先順序:" + thread.getPriority());  	}  }    結果:  執行緒唯一標識符:9  執行緒名稱:Thread-0  run  執行緒狀態:RUNNABLE