愛生活,愛編碼,微信搜一搜【架構技術專欄】關注這個喜歡分享的地方。 本文 架構技術專欄 已收錄,有各類視頻、資料以及技術文章。java
1、何時應該使用多線程?
今天看到一個問題,忽然有感而發,想聊下這個話題。數據庫
不知道你們有沒有想過這個問題,就是何時我該使用多線程呢?使用多線程就必定會提高系統性能嗎?編程
一、實際上是否應該使用多線程在很大程度上取決於應用程序的類型。安全
-
計算密集型(如純數學運算) 的, 並受CPU 功能的制約, 則只有多CPU(或者多個內核) 機器可以從更多的線程中受益, 單CPU下, 多線程不會帶來任何性能上的提高, 反而有可能因爲線程切換等額外開銷而致使性能降低微信
-
IO密集型的,當個人應用必須等待緩慢的資源(如網絡鏈接或者數據庫鏈接上返回數據)時,那麼多線程會讓系統的CPU充分的利用起來,當一個線程阻塞掛起時,另外一個線程能夠繼續使用CPU資源。網絡
-
其實,就是多線程不會增長CPU的處理能,而是可以更加充分地利用CPU資源。多線程
因爲同一進程的多個線程是共享同一片內存資源的,在帶來方便的同時也必然會增長其複雜性,如何保證多線程訪問數據的一致性問題等。而多線程屬於編程中容易翻車的地方。而且多線程編程問題的測試定位也是比較難的。整體來講,好的多線程是寫出來,將多線程問題寄但願於測試中發現, 無疑是極度不可靠的。SO,努力的學習吧。架構
Java API 與多線程息息相關的的幾大關鍵字:volatile、synchronized、 wait、 notify. 理解了這幾個關鍵字,就能夠編寫多線程的代碼了。性能
2、何時須要加鎖?
在多線程場合下,最重要的就是保障數據的一致性問題,而保障數據一致性問題,就須要藉助於鎖了。學習
其實咱們在多線程的場景下應該搞清楚一個問題,就是到底什麼須要保護?並非全部的的數據都須要加鎖保護,只有那些涉及到被多線程訪問的共享的數據才須要加鎖保護。
鎖的本質其實就是確保在同一時刻,只有一個線程在訪問共享數據,那麼此時該共享數據就能獲得有效的保護。
舉例說明下,好比咱們想構造一個多線程下安全的單向鏈表:
<img src="/Users/luqiang/Downloads/公衆號圖片/鏈表插入新圖.jpg" alt="鏈表插入新圖" style="zoom:50%;" />
假如如今有兩個線程在操做這個鏈表,一個寫線程插入一個新元素7,另外一個讀線程遍歷鏈表數據,若是不使用任何鎖,那就有可能出現下面的執行順序:
步驟 | 寫線程 | 讀線程 |
---|---|---|
0 | 修改鏈表元素2的next指針指向7這個元素 | ... ... |
1 | ... ... | 遍歷鏈表,到7的時候發現next 爲null,遍歷結束 |
2 | 修改元素7的next 指針指向3 | ... ... |
經過上面的例子咱們能夠明顯看到在多線程下操做這個鏈表,有可能會致使讀線程讀到的數據不完整,只有從鏈表頭部到元素7的位置的數據。因而可知,不加入任何保護措施的多線程保護,勢必會致使數據的混亂。爲了不數據一致性問題,咱們就須要將操做該隊列的代碼放入同步塊內(鎖的對象也就是這個鏈表實例),來確保同一時刻只有一個線程能夠訪問該鏈表。
如何加鎖?
這裏簡單的說下,通常咱們都是使用synchronized(若是沒有特殊需求建議直接使用這個關鍵字,jdk新版本它真的很快),記住synchronized 鎖的就是對象頭。
簡單的說下,主要有下面幾種用法:
-
synchronized 放在方法上,鎖的是當前synchronized 方法的對象實例
-
synchronized在static 方法上,鎖的是synchronized 方法的class 類對象,注意這裏class 其實也是一個對象。
-
synchronized(this)在代碼塊中,鎖的是代碼塊括號內的對象,這裏this指的就是調用這個方法的類實例對象
3、 多線程中易犯的錯誤
一、鎖範圍過大
共享資源訪問完成後, 後續的代碼沒有放在synchronized同步代碼塊以外。 會致使當前線程長期無效的佔用該鎖, 而其它爭用該鎖的線程只能等待, 最終致使性能受到極大影響。
public void test() { synchronized(lock){ ... ... //正在訪問共享資源 ... ... //作其它耗時操做,但這些耗時操做與共享資源無關 } }
面對上面這種寫法,會致使此線程長期佔有此鎖,從而致使其餘線程只能等待,下面來討論下解決方法:
1)單CPU場景下,將不須要同步的耗時操做拿到同步塊外面,有的狀況能夠提高性能,有的卻不行。
-
CPU密集型的代碼 ,不存在磁盤IO/網絡IO等低CPU消耗的代碼。 這種狀況下, CPU 99%都在執行代碼。 所以縮小同步塊也不會帶來任何性能上的提高, 同時縮小同步塊也不會帶來性能上的降低。
-
IO密集型的代碼,在執行不消耗CPU的代碼時,其實CPU屬於空閒狀態的。若是此時讓CPU工做起來就能夠帶來總體上性能的提高。因此在這種狀況下,就能夠將不須要同步的耗時操做移到同步塊外面了。
2)多CPU場景下,將耗時的CPU操做拿到同步塊外面,老是能夠提高性能的
-
CPU密集型的代碼,不存在IO操做等不消耗CPU的代碼片斷。由於當前是多CPU,其餘CPU也多是空閒的。因此在縮小同步塊的時候,也會讓其餘線程儘快的執行這段代碼從而帶來性能上的提高。
-
IO密集型的代碼,由於當前PCU都是空閒的狀態,因此將耗時的操做放在同步塊外面,必定會帶來總體上的性能提高。
固然,無論怎麼樣,縮小鎖的同步範圍對於系統來講都是百利而無一害的,所以上面的代碼應該改成:
public void test() { synchronized(lock){ ... ... //正在訪問共享資源 } ... ... //作其它耗時操做,但這些耗時操做與共享資源無關 }
綜上所述,一個重點,就是隻將訪問共享資源的代碼放在同步塊內,保證快進快出。
二、死鎖的問題
死鎖要知道的:
-
死鎖,簡單地說就是兩個線程或多個線程在同時等待被對方持有的鎖致使的,死鎖會致使線程沒法繼續執行並被永久掛起。
-
若是線程發生了死鎖,那咱們就能從線程堆棧中明顯的看到」Found one Java-level deadlock「,而且線程棧還會給出死鎖的分析結果。
-
死鎖這種問題若是發生在關鍵系統上就可能會致使系統癱瘓,若是想要快速恢復系統,臨時惟一的方法就是保留線程棧先重啓,而後再儘快的恢復。
-
死鎖這種問題有時候測試是很難被當即發現的,不少時候在測試時可否及時發現這類問題,就全看你的運氣和你準備的測試用例了。
-
避免死鎖這類問題,惟一的辦法就是改代碼。但一個可靠的系統是設計出來的,而不是經過改BUG改出來的,當出現這種問題的時候就須要從系統設計角度去分析了。
-
有人會認爲死鎖會致使CPU 100%,其實對也不對。 要看使用的什麼類型的鎖了,好比synchronized致使的死鎖,那就不會致使CPU100%,只會掛起線程。但若是是自旋鎖這種纔可能會消耗CPU。
三、共用一把鎖的問題
就是多個共享變量會共用一把鎖,特別是在方法級別上使用synchronized,從而人爲致使的鎖競爭。
上例子,下面是新手容易犯的錯誤:
1 public class MyTest 2 { 3 Object shared; 4 synchronized void fun1() {...} //訪問共享變量shared 5 synchronized void fun2() {...} //訪問共享變量shared 6 synchronized void fun3() {...} //不訪問共享變量shared 7 synchronized void fun4() {...} //不訪問共享變量shared 8 synchronized void fun5() {...} //不訪問共享變量shared 9 }
上面的代碼每個方法都被加了synchronized ,明顯違背了保護什麼鎖什麼的原則。
3、線程數咱們通常設多少比較合理呢?
其實你們都知道,在大多數場合下多線程都是能夠提升系統的性能和吞吐量,但一個系統到底多少個線程纔是合理的?
總的來講,線程數量太多太少其實都不太好,多了會由於線程頻繁切換致使開銷增大,有時候反而下降了系統性能。少了又會致使CPU資源不能充分的利用起來,性能沒有達到瓶頸。
因此,系統到底使用多少線程合適,是要看系統的線程是否能充分的利用了CPU。其實實際狀況,是不少時候不消耗CPU,如:磁盤IO、網絡IO等。
磁盤IO、網絡IO相比CPU的速度,那簡直是至關的慢的,在執行IO的這段時間裏CPU實際上是空閒的。若是這時其餘線程能把這空閒的CPU利用上,就能夠達到提示系統性能和吞吐的目的。
其實上面咱們也提到過,也就是兩種計算特性:
CPU密集型: 由於每一個CPU都是高計算負載的狀況,若是設置過多的線程反而會產生沒必要要的上下文切換。因此,通常線程咱們會設置 CPU 核數 + 1就能夠了,爲啥要加1 呢,即便當計算(CPU)密集型的線程偶爾因爲頁缺失故障或者其餘緣由而暫停時,這個「額外」的線程也能確保 CPU 的時鐘週期不會被浪費,其實就是個備份。
IO密集型:由於大量的IO操做,會致使CPU處於空閒狀態,因此這時咱們能夠多設置些線程。 因此, 線程數 = CPU 核心數 * (1+ IO 耗時/CPU 耗時) 就能夠了,但願能給你點啓發。
愛生活,愛編碼,微信搜一搜【架構技術專欄】關注這個喜歡分享的地方。 本文 架構技術專欄 已收錄,有各類視頻、資料以及技術文章。