趣談併發 2:認識併發程式設計的利與弊
讀完本文你將瞭解:
從上篇文章 趣談併發(1):全面認識 Thread 我們瞭解了 Java 中執行緒的基本概念和關鍵方法。
在開始使用執行緒之前,我覺得我們有必要先了解下多執行緒給我們帶來的好處與可能造成的損失,這樣才能在合適的地方選用合適的併發策略。
多執行緒的優點
1:提高資源利用率
“一口多用”其實就是一種多執行緒。
想象一下,我們左手拿著海鮮大狂歡披薩,右手拿著意式麵包佐蘆筍臘肉腸,桌子上還放著青檸香茅飲,左邊吃一口,右邊咬一塊,再使勁地喝一口,啊!此生無憾!
看到了吧,多執行緒最大的優點就是:提高資源利用率。
在 PC 或者手機中,我們的資源主要說的就是 CPU。
我們知道,通常情況下,網路和磁碟的 I/O 比 CPU 和記憶體的 IO 慢的多。
在執行頻繁 I/O 的任務時,CPU 很多時候都處於閒置狀態。這時如果我們開啟多個執行緒,在 A 執行緒 I/O 的同時讓 CPU 執行 B,在 B 執行緒 I/O 的同時再執行 A。這樣就比 A B 序列執行時 CPU 的利用率更高。
2:響應更快
這一點想必小肉深有感悟:
- 家裡快遞來了,小肉會說:shixin,去取一下。我下去愚公移山的時候,她可以繼續 shopping;
- 窗外有人吼賣櫻桃嘍,小肉會說:shixin,去買一點。我去夸父逐日的時候,她可以繼續吃吃吃。
我們在主執行緒接受使用者請求後,將耗時操作交給子執行緒,然後告訴使用者在等待的同時還可以乾點別的。
此外將一些可以拆分的任務分給多個執行緒執行,執行完畢後再合併結果,也會讓任務處理更高效。
多執行緒的缺點
俗話說:有陽光的地方就有黑暗;
俗話說:世界上沒有免費的午餐。
執行緒能夠給我們帶來以上好處,是需要一定代價的。
1:增加資源消耗
每個執行緒都擁有各自的計數器、堆疊、區域性變數等資源,同時管理這些執行緒也需要額外的資源。
2:上下文切換的開銷
當 CPU 排程不同執行緒時,它需要更新當前執行執行緒的資料,程式指標,以及下一個執行緒的相關資訊。
這種切換會有額外的時間、空間消耗,我們在開發中應該避免頻繁的執行緒切換。
3:設計、編碼、測試的複雜度增加
其實第三點才是關鍵,我們知道公司人數越多問題越多,執行緒也一樣,執行緒之間的互動非常複雜。
不正確的執行緒同步只有執行時才能發現問題,而且非常難以重現,發現並修復複雜度大大增加。
Java 記憶體模型與 CPU 記憶體簡介
在瞭解多個執行緒同時訪問資料可能出現的問題之前,我們需要先了解 Java 記憶體模型。
Java 記憶體模型規範了 Java 虛擬機器與計算機記憶體是如何協同工作的。
Java 記憶體模型中將 JVM 分為堆和棧:
- 堆為同一個 JVM 中所有執行緒共享,存放執行時建立的物件和陣列資料;
- 棧為每個執行緒獨有,棧中存放了當前方法的呼叫資訊以及基本資料型別和引用型別的資料。
Java 中的堆
堆在虛擬機器啟動時建立,堆佔用的記憶體由垃圾回收器管理,不需要我們手動回收。
JVM 沒有規定死必須使用哪一種記憶體回收機制,不同的虛擬機器實現可以使用不同的回收演算法。
堆中包含在 Java 程式中建立的所有物件,無論是哪一個執行緒建立的。
一個物件的成員變數隨著這個物件自身存放在堆上。不管這個成員變數是基本型別還是引用型別。
靜態成員變數跟隨著類定義一起也存放在堆上。
Java 中的棧
棧線上程建立時建立,它和 C 語言中的棧相似,在一個方法中,你建立的區域性變數和部分結果都會儲存在棧中,並在方法呼叫和返回中起作用。
當前棧只對當前執行緒可見。即使兩個執行緒執行同樣的程式碼,這兩個執行緒仍然會在自己的執行緒棧中建立一份本地副本。
因此,每個執行緒擁有每個本地變數的獨有版本。
棧中儲存方法呼叫棧、基本型別的資料、以及物件的引用。
計算機中的記憶體、暫存器、快取
一個現代計算機通常由兩個或者多個 CPU,每個 CPU 都包含一系列的暫存器,CPU 在暫存器上執行操作的速度遠大於在主存上執行的速度。
每個 CPU 可能還有一個 CPU 快取層。CPU 訪問快取層的速度快於訪問主存的速度,但通常比訪問內部暫存器的速度還要慢一點。
-
通常情況下,當一個 CPU 需要讀取主存時,它會將主存的部分讀到 CPU 快取中。它甚至可能將快取中的部分內容讀到它的內部暫存器中,然後在暫存器中執行操作。
-
當 CPU 需要將結果寫回到主存中去時,它會將內部暫存器的值重新整理到快取中,然後在某個時間點將值重新整理回主存。
這裡先簡單地對“Java 記憶體模型”進行介紹,後序介紹完常見併發類後再詳細總結。
多執行緒可能出現的問題
通過上述介紹,我們可以知道,如果多個執行緒共享一個物件,每個執行緒在自己的棧中會有物件的副本。
如果執行緒 A 對物件中的某個變數進行修改後還沒來得及寫回主存,執行緒 B 也對該變數進行了修改,那最後重新整理回主記憶體後的值一定和期望的值不一致。
就好比拭心和小翔同時開發同一模組程式碼,拭心下筆如有神不一會兒搞定了註冊登入並且提交,小翔沒有從伺服器拉程式碼就矇頭狂寫,最後一 pull 程式碼,就會發現自己寫的好多都跟伺服器上的衝突了!
競態條件與臨界區
當多個執行緒操作同一資源時,如果對資源的訪問順序敏感,就稱存在競態條件。導致競態條件發生的程式碼區稱作臨界區。
在臨界區中使用適當的同步就可以避免競態條件,比如 synchronized, 顯式鎖和原子操作類等。
記憶體可見性
拭心寫的程式碼小翔無法立即看到,這就是所謂的“記憶體可見性”問題。
為了讓執行緒 A 對變數做的修改執行緒 B 立即可以看到,我們可以使用 volatile 修飾變數或者對修改操作使用同步。
總結
本篇文章結合 Java 記憶體模型簡單介紹了多執行緒開發的優點與可能導致的問題,猶豫了一下我還是覺得有必要在開始學習 Java 各種併發 API 之前瞭解它們出現的背景,這樣更容易明白它們解決了什麼問題。
知道了多執行緒的開銷與可能帶來的問題後,我們在開發中不要為了使用多執行緒而使用多執行緒。應該在確認多執行緒給專案帶來的好處比隱含的開銷更多時,再使用多執行緒。
1歡迎工作一到十年的Java工程師朋友們加入Java進階高階架構:828545509
2本群提供免費的學習指導 架構資料 以及免費的解答
3不懂得問題都可以在本群提出來 之後還會有職業生涯規劃以及面試指導