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