1. 程式人生 > >併發程式設計2:認識併發程式設計的利與弊

併發程式設計2:認識併發程式設計的利與弊

讀完本文你將瞭解:

多執行緒的優點 1提高資源利用率 2響應更快 多執行緒的缺點 1增加資源消耗 2上下文切換的開銷 3設計編碼測試的複雜度增加 Java 記憶體模型與 CPU 記憶體簡介 Java 中的堆 Java 中的棧 計算機中的記憶體暫存器快取 多執行緒可能出現的問題 競態條件與臨界區 記憶體可見性 總結 35 追加 Thanks 從上篇文章 併發程式設計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:增加資源消耗 每個執行緒都擁有各自的計數器、堆疊(stack)、區域性變數等資源,同時管理這些執行緒也需要額外的資源。

2:上下文切換的開銷 當 CPU 排程不同執行緒時,它需要更新當前執行執行緒的資料,程式指標,以及下一個執行緒的相關資訊。

這種切換會有額外的時間、空間消耗,我們在開發中應該避免頻繁的執行緒切換。

3:設計、編碼、測試的複雜度增加 其實第三點才是關鍵,我們知道公司人數越多問題越多,執行緒也一樣,執行緒之間的互動非常複雜。

不正確的執行緒同步只有執行時才能發現問題,而且非常難以重現,發現並修復複雜度大大增加。

Java 記憶體模型與 CPU 記憶體簡介 在瞭解多個執行緒同時訪問資料可能出現的問題之前,我們需要先了解 Java 記憶體模型。

Java 記憶體模型規範了 Java 虛擬機器與計算機記憶體是如何協同工作的。

Java 記憶體模型中將 JVM 分為堆和棧:

堆為同一個 JVM 中所有執行緒共享,存放執行時建立的物件和陣列資料; 棧為每個執行緒獨有,棧中存放了當前方法的呼叫資訊以及基本資料型別和引用型別的資料。

Java 中的堆 堆在虛擬機器啟動時建立,堆佔用的記憶體由垃圾回收器管理,不需要我們手動回收。

JVM 沒有規定死必須使用哪一種記憶體回收機制,不同的虛擬機器實現可以使用不同的回收演算法。

堆中包含在 Java 程式中建立的所有物件,無論是哪一個執行緒建立的。

一個物件的成員變數隨著這個物件自身存放在堆上。不管這個成員變數是基本型別還是引用型別。

Java 中的棧 棧線上程建立時建立,它和 C 語言中的棧相似,在一個方法中,你建立的區域性變數和部分結果都會儲存在棧中,並在方法呼叫和返回中起作用。

當前棧只對當前執行緒可見。即使兩個執行緒執行同樣的程式碼,這兩個執行緒仍然會在自己的執行緒棧中建立一份本地副本。

因此,每個執行緒擁有每個本地變數的獨有版本。

棧中儲存方法呼叫棧、基本型別的資料、以及物件的引用。

計算機中的記憶體、暫存器、快取 這部分摘自:http://ifeve.com/java-memory-model-6/

一個現代計算機通常由兩個或者多個 CPU,每個 CPU 都包含一系列的暫存器,CPU 在暫存器上執行操作的速度遠大於在主存上執行的速度。

每個 CPU 可能還有一個 CPU 快取層。CPU 訪問快取層的速度快於訪問主存的速度,但通常比訪問內部暫存器的速度還要慢一點。

通常情況下,當一個 CPU 需要讀取主存時,它會將主存的部分讀到 CPU 快取中。它甚至可能將快取中的部分內容讀到它的內部暫存器中,然後在暫存器中執行操作。

當 CPU 需要將結果寫回到主存中去時,它會將內部暫存器的值重新整理到快取中,然後在某個時間點將值重新整理回主存。

這裡先簡單地對“Java 記憶體模型”進行介紹,後序介紹完常見併發類後再詳細總結。

多執行緒可能出現的問題 通過上述介紹,我們可以知道,如果多個執行緒共享一個物件,每個執行緒在自己的棧中會有物件的副本。

如果執行緒 A 對物件中的某個變數進行修改後還沒來得及寫回主存,執行緒 B 也對該變數進行了修改,那最後重新整理回主記憶體後的值一定和期望的值不一致。

就好比拭心和小翔同時開發同一模組程式碼,拭心下筆如有神不一會兒搞定了註冊登入並且提交,小翔沒有從伺服器拉程式碼就矇頭狂寫,最後一 pull 程式碼,就會發現自己寫的好多都跟伺服器上的衝突了!

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

在臨界區中使用適當的同步就可以避免競態條件,比如 synchronized, 顯式鎖和原子操作類等。

記憶體可見性 拭心寫的程式碼小翔無法立即看到,這就是所謂的“記憶體可見性”問題。

為了讓執行緒 A 對變數做的修改執行緒 B 立即可以看到,我們可以使用 volatile 修飾變數或者對修改操作使用同步。

總結 本篇文章結合 Java 記憶體模型簡單介紹了多執行緒開發的優點與可能導致的問題,猶豫了一下我還是覺得有必要在開始學習 Java 各種併發 API 之前瞭解它們出現的背景,這樣更容易明白它們解決了什麼問題。

知道了多執行緒的開銷與可能帶來的問題後,我們在開發中不要為了使用多執行緒而使用多執行緒。應該在確認多執行緒給專案帶來的好處比隱含的開銷更多時,再使用多執行緒。

2017.3.5 追加 感謝遙望江南2009的提問,他的認真促使我仔細查閱了一些資料,在這裡記錄下關於執行緒裡容易混淆的一些概念:

1.JVM 中每個執行緒操作操作物件的話會拷貝主執行緒的物件到自己執行緒的哪裡?

當執行緒訪問某一個物件時候值的時候:  首先通過物件的引用找到對應在堆記憶體的變數的值;  然後把堆記憶體變數的具體值 load 到執行緒工作記憶體中,建立一個變數副本;  之後執行緒就不再和物件在堆記憶體變數值有任何關係,而是直接修改副本變數的值,在修改完之後也不會立即同步修改共享堆記憶體中該變數的值;  直到某一個時刻(執行緒退出之前),自動把執行緒變數副本的值回寫到物件在堆中變數。這樣在堆中的物件的值就產生變化了。

上面說的執行緒工作記憶體是 JVM 的一個抽象概念,具體是哪裡,JLS 也沒有說明,但可以肯定的是,這裡關於執行緒的“working memory”對許多平臺來說都是對快取記憶體的抽象。規範不能把implementation-specific 的東西寫進去,不然會影響實現難度。

2.在JAVA中,有六個不同的地方可以儲存資料:

暫存器(register)  這是最快的儲存區,因為它位於不同於其他儲存區的地方——處理器內部。但是暫存器的數量極其有限,所以暫存器由編譯器根據需求進行分配。你不能直接控制,也不能在程式中感覺到暫存器存在的任何跡象。 堆疊(stack)  位於通用RAM中,但通過它的“堆疊指標”可以從處理器哪裡獲得支援。堆疊指標若向下移動,則分配新的記憶體;若向上移動,則釋放那些 記憶體。這是一種快速有效的分配儲存方法,僅次於暫存器。建立程式時候,JAVA編譯器必須知道儲存在堆疊內所有資料的確切大小和生命週期,因為它必須生成 相應的程式碼,以便上下移動堆疊指標。這一約束限制了程式的靈活性,所以雖然某些JAVA資料儲存在堆疊中——特別是物件引用,但是JAVA物件不儲存其 中。 堆(heap)  一種通用性的記憶體池(也存在於RAM中),用於存放所以的JAVA物件。堆不同於堆疊的好處是:編譯器不需要知道要從堆裡分配多少儲存區 域,也不必知道儲存的資料在堆裡存活多長時間。因此,在堆裡分配儲存有很大的靈活性。當你需要建立一個物件的時候,只需要new寫一行簡單的程式碼,當執行 這行程式碼時,會自動在堆裡進行儲存分配。當然,為這種靈活性必須要付出相應的程式碼。用堆進行儲存分配比用堆疊進行儲存儲存需要更多的時間。 靜態儲存(static storage)  這裡的“靜態”是指“在固定的位置”。靜態儲存裡存放程式執行時一直存在的資料。你可用關鍵字static來標識一個物件的特定元素是靜態的,但JAVA物件本身從來不會存放在靜態儲存空間裡。 常量儲存(constant storage  常量值通常直接存放在程式程式碼內部,這樣做是安全的,因為它們永遠不會被改變。有時,在嵌入式系統中,常量本身會和其他部分分割離開,所以在這種情況下,可以選擇將其放在ROM中 非RAM儲存  如果資料完全存活於程式之外,那麼它可以不受程式的任何控制,在程式沒有執行時也可以存在。 就速度來說,有如下關係: 暫存器 > 堆疊 > 堆 > 其他

(摘自《Thinking in Java》)

歡迎掃描關注微信公眾號“安卓進化論”,向高手進擊!

Thanks https://en.wikipedia.org/wiki/Java_memory_model  https://en.wikipedia.org/wiki/Java_concurrency#Memory_model  http://tutorials.jenkov.com/java-concurrency/costs.html  http://ifeve.com/java-concurrency-thread-directory/  https://www.zhihu.com/question/29833675  http://www.iteye.com/problems/24814