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


