趣談并發 2:認識并發編程的利與弊
在開始使用線程之前,我覺得我們有必要先了解下多線程給我們帶來的好處與可能造成的損失,這樣才能在合適的地方選用合適的并發策略。
多線程的優點
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 語言中的棧相似,在一個方法中,你創建的局部變量和部分結果都會保存在棧中,并在方法調用和返回中起作用。
當前棧只對當前線程可見。即使兩個線程執行同樣的代碼,這兩個線程仍然會在自己的線程棧中創建一份本地副本。
因此,每個線程擁有每個本地變量的獨有版本。
棧中保存方法調用棧、基本類型的數據、以及對象的引用。
計算機中的內存、寄存器、緩存
這部分摘自: 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 之前了解它們出現的背景,這樣更容易明白它們解決了什么問題。
知道了多線程的開銷與可能帶來的問題后,我們在開發中不要為了使用多線程而使用多線程。應該在確認多線程給項目帶來的好處比隱含的開銷更多時,再使用多線程。
來自:http://blog.csdn.net/u011240877/article/details/58756137