深刻理解Java虛擬機——第十三章——高效併發

線程安全

當多個線程訪問一個對象時,若是不考慮這些線程在運行環境下的調度和交替執行,也不須要進行額外的同步,或者在調用地方進行額外的協調操做,調用這個對象的行爲均可以得到正確的結果,那麼這個對象是線程安全的。web

Java的線程安全

各類操做共享的數據分類5類:安全

  1. 不可變:不可變對象必定是線程安全的。如基本數據類型定義爲final就是不可變。String、Number、Long等數值都是不可變的,但Number的原子類AtomicInteger並不是不可變的。
  2. 絕對線程安全:無論運行時環境如何,都不須要任何額外的同步措施。如Vector類雖然是線程安全的,由於其方法都加上了synchronized,可是若是在外面用兩個線程調用它,一個訪問元素一個刪除元素,那麼若是外面不使用同步的話就會使得訪問到剛被刪除的元素而報錯。所以Vector不是絕對線程安全。
  3. 相對線程安全:保證對象單獨操做是線程安全的。如Vector就是相對線程安全的。
  4. 線程兼容:對象自己不是線程安全的,可是調用的時候用同步手段能夠保證線程安全。如ArrayList和HashMap。
  5. 線程對立:不管是否採起同步措施,都沒法在多線程環境中併發使用的代碼。如Thread的suspend()和resume()方法,同時操做一個線程進行中斷和恢復,一旦併發,不管是否同步都是存在死鎖風險。即一個線程suspend掛起本身(suspend不會釋放所持有的鎖除非進行resume),若是resume所在的線程在resume以前因想要獲取suspend線程裏的鎖而阻塞,那麼就發生了死鎖。參考https://www.jianshu.com/p/7a123f212ca1

線程安全的實現方法

互斥同步

互斥是實現同步的一種手段。須要阻塞和喚醒,使得性能下降,也稱阻塞同步,是悲觀鎖。不管共享數據是否會發生競爭都須要加鎖。服務器

最基本互斥手段是synchronized。會在同步塊的先後造成monitorenter和monitorexit,在執行monitorenter時嘗試去獲取鎖,若是已被本身持有或者未被持有則鎖計數器加1,執行monitorexit就會減1,直到爲0才釋放鎖。若是獲取鎖失敗則會處於阻塞狀態。多線程

除了synchronnized外還有JUC的重入鎖(ReentrantLock),都具有線程重入特性,只是代碼寫法上有區別,一個爲API層面的互斥鎖(lock和unlock方法配合try/finally語句實現),一個是原生語法層面的互斥鎖。RenetrantLock多了3個高級功能:併發

  • 等待可中斷:當持有鎖的線程長期不釋放鎖的時候,等待鎖的線程能夠放棄等待,改成處理其它事情
  • 公平鎖:多個線程在等待一個鎖時,按申請的時間順序來得到鎖。而非公平鎖是每一個等待線程都有機會得到鎖,synchronized是非公平鎖
  • 鎖綁定條件:一個Reentrant能夠綁定多個Condition對象,而synchronized要和多於一個條件關聯的時候,就不得不額外添加一個鎖,Reentrant只需屢次調用newCondition。

非阻塞同步

樂觀鎖。其併發策略時先進行操做,若是沒有線程用共享數據,那操做就成功;若是有爭用,那麼產生衝突,就採起其它措施補償(常見的補償是不斷重試直到成功爲止)。所以就不要把線程掛起。app

樂觀鎖須要保證操做和衝突檢測具備原子性,若是使用互斥同步來保證就失去意義了,所以須要硬件來完成這件事。如CAS指令。性能

CAS有三個操做數:內存地址A、舊的預期值O、新值N。當且僅當A符合舊預期值O時才進行更新V爲新值N,並返回舊值O,若是不符合則不進行更新。該操做是一個原子操做。優化

無同步方案

保證線程安全不必定就須要同步,兩者無因果關係。若是一個方法不涉及共享數據,那麼就無須任何同步。例以下面兩類代碼是天生線程安全的:spa

  • 可重入代碼:也叫純代碼,能夠在代碼執行的任什麼時候候中斷,轉而去執行另外一段代碼(包括遞歸調用本身),在控制權返回後原來的程序不會出現錯誤。可重入代碼是都是線程安全的,反過來就不是。有個簡單的判斷方法:若是方法的返回結果是可預測的,只要輸入相同的數據都能返回相同的結果,那麼就知足可重入的要求。例如不依賴堆上的數據和公用的系統資源、不調用不可重入代碼、用到的狀態量都是參數傳入。
  • 線程本地存儲:若是代碼所需的數據必須與其它代碼共享,那麼就看是否能保證將這些共享數據的代碼在同一個線程中執行。例如消費隊列都是保證消費過程在一個線程中完成,具體爲web中一個請求對應一個服務器線程。

鎖優化

鎖優化技術:自適應自旋、鎖消除、鎖粗化、輕量級鎖、偏向鎖等、操作系統

自旋鎖和自適應自旋

互斥同步最大的性能消耗就是線程的掛起和恢復,都須要轉入內核態完成。大部分共享數據的鎖定狀態只持續很短的時間,所以爲了這段時間取掛起和恢復不值得,因此可讓後面請求的鎖「等待一下」,但不放棄處理器的執行時間,只需執行忙循環(自旋),這就是所謂的自旋鎖。

自旋不能代替阻塞。若是自旋的時間開銷超過了線程掛起和恢復的時間開銷,那麼就白白消耗處理器資源。默認自旋10次,可-XX:PreBlockSpin更改。JDK1.6後引入自旋鎖,自旋時間不是固定的,而是由上一次在同一個鎖上的自旋時間以及鎖的擁有者狀態來決定。若是上一次自旋成功且線程正在運行中,則會認爲這次自旋可能成功,所以會讓自旋持續更長時間,若是一個鎖的自旋不多成功則之後獲取可能直接省略自旋直接掛起。

鎖消除

鎖消除是指在即時編譯時,對一些代碼上要求同步,可是被檢測到不存在共數據競爭的鎖進行消除。例如StringBuffer的append有同步塊,其引用sb不會逃逸到concatString外,其餘線程沒法訪問到sb來進行操做,所以鎖能夠被安全消除。sb放到實例變量去就得加鎖處理。

public String concatString(String s1, String s2, String s3) {
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        sb.append(s3);
        return sb.toString();
}

鎖粗化

原則上是推薦將同步的做用範圍變小,只在共享數據的實際做用域才發生同步。但若是在一系列操做對同一個對象反覆加鎖解鎖,甚至加鎖出如今for循環裏,那麼即便沒競爭,頻繁的互斥同步也會致使性能損耗。例如上面的append,虛擬機就會把加鎖範圍變成第一個append擴展到最後一個append。

輕量級鎖

輕量級鎖是相對使用操做系統互斥量來實現的傳統鎖而言的,傳統鎖是重量級鎖。輕量級鎖是減小使用操做系統的互斥量產生的性能損耗。(使用自旋鎖)

若是一開始線程獲取鎖時,對象頭標誌未鎖定,則用CAS操做使對象頭標誌爲輕量鎖,則擁有了該對象的鎖。若是更新失敗,則判斷對象的mark word是否指向當前線程的棧幀,是則代表已經擁有這個鎖,直接進入同步代碼塊,不然說明該鎖被其它線程佔了。若是兩條以上的線程爭同一個鎖,那麼輕量級鎖就變成重量級鎖。

輕量級鎖提高同步性能的依據是「大部分鎖,在整個同步週期是不存在競爭的」,這是一個經驗數據。沒有競爭,則使用CAS操做就避免了使用互斥的開銷。

偏向鎖

鎖會偏向於第一個獲取它的線程,若是在接下來的執行過程當中,鎖沒被其它線程獲取,則持有偏向鎖的線程不須要再進行同步。當另一個線程嘗試獲取該鎖時,則根據鎖對象是否被鎖定轉換爲輕量級鎖或未鎖定狀態。

相關文章
相關標籤/搜索