本文已經收錄到個人 Github 我的博客,歡迎大佬們光臨寒舍:html
個人 GIthub 博客git
以前咱們學習了內存模型和線程,瞭解了
JMM
和線程,初步探究了JVM
怎麼實現併發,而本篇文章,咱們的關注點是JVM
如何實現高效程序員
併發編程的目的是爲了讓程序運行得更快,提升程序的響應速度,雖然咱們但願經過多線程執行任務讓程序運行得更快,可是同時也會面臨很是多的挑戰,好比像線程安全問題、線程上下文切換的問題、硬件和軟件資源限制等問題,這些都是併發編程給咱們帶來的難題。github
其中線程安全問題是咱們最關心的問題之一,咱們接下來主要就圍繞着線程安全的問題來展開。編程
當多個線程訪問一個對象時,若是不用考慮這些線程在運行時環境下的調度和交替執行,也不須要進行額外的同步,或者在調用方進行任何其餘的協調操做,調用這個對象的行爲均可以得到正確的結果,那這個對象是線程安全的數組
要求線程安全的代碼都必須具有一個特徵: 代碼自己封裝了全部必要的正確性保障手段(如互斥同步等),令調用者無須關心多線程的問題,更無須本身採起任何措施來保證多線程的正確調用。安全
下面將按照線程安全的程度由強至弱分紅五類多線程
必定是線程安全的併發
如何實現:函數
1.若是共享數據是一個基本數據類型,只要在定義時用
final
關鍵字修飾2.若是共享數據是一個對象,最簡單的方法是把對象中帶有狀態的變量都聲明爲
final
(例如String
類的實現)
- 是一般意義上所講的線程安全
- 大部分的線程安全類都屬於這種類型,如
Vector
、HashTable
、Collections#synchronizedCollection()
包裝的集合等
- 是一般意義上所講的非線程安全
Java API
中大部分類都是屬於線程兼容的,如ArrayList
和HashMap
等
例子:
Thread
類的suspend()
和resume()
,一個嘗試中斷線程,一個嘗試恢復線程,在併發條件下,有可能會形成死鎖
可分紅兩大手段:
- 經過代碼編寫實現線程安全
- 經過虛擬機自己實現同步與鎖
本篇重點在虛擬機自己
同步:在多個線程併發訪問共享數據時,保證共享數據在同一個時刻只被一個線程使用
互斥:是實現同步的一種手段,臨界區、互斥量和信號量都是主要的互斥實現方式
二者關係:互斥是因,同步是果;互斥是方法,同步是目的
屬於悲觀併發策略(悲觀鎖),即認爲只要不作正確的同步措施就確定會出現問題,所以不管共享數據是否真的會出現競爭,都要加鎖
最大的問題是進行線程阻塞和喚醒所帶來的性能問題,也稱爲阻塞同步
使用方式:
A.使用 synchronized
關鍵字:
原理:編譯後會在同步塊的先後分別造成
monitorenter
和monitorexit
這兩個字節碼指令,並經過一個reference
類型的參數來指明要鎖定和解鎖的對象注意:
1.若明確指定了對象參數,則取該對象的
reference
2.不然,會根據
synchronized
修飾的是實例方法仍是類方法去取對應的對象實例或Class
對象來做爲鎖對象過程:執行
monitorenter
指令時先要嘗試獲取對象的鎖。若該對象沒被鎖定或者已被當前線程獲取,那麼鎖計數器+ 1
;而在執行monitorexit
指令時,鎖計數器- 1
;當鎖計數器 =0
時,鎖就被釋放;若獲取對象鎖失敗,那當前線程會一直被阻塞等待,直到對象鎖被另一個線程釋放爲止特別注意:
1.
synchronized
同步塊對同一條線程來講是可重入的,不會出現自我鎖死的問題2.同步塊在已進入的線程執行完以前,會阻塞後面其餘線程的進入
B.使用重入鎖 ReentrantLock
:
以前在 進階之路 | 奇妙的 Thread 之旅中也提到太重入鎖的使用,相信看過的讀者還有一些印象
與
synchronized
的相同:用法與synchronized
很類似,且均可重入與
synchronized
的不一樣:1.等待可中斷:當持有鎖的線程長期不釋放鎖的時候,正在等待的線程能夠選擇放棄等待,改成處理其餘事情
2.公平鎖:多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次得到鎖。而
synchronized
是非公平的,即在鎖被釋放時,任何一個等待鎖的線程都有機會得到鎖。ReentrantLock
默認狀況下也是非公平的,但能夠經過帶布爾值的構造函數改用公平鎖3.鎖綁定多個條件:一個
ReentrantLock
對象能夠經過屢次調用newCondition()
同時綁定多個Condition
對象。而在synchronized
中,鎖對象的wait()
和notify()
或notifyAl()
只能實現一個隱含的條件,若要和多於一個的條件關聯不得不額外地添加一個鎖
synchronized
能實現需求的狀況下,優先考慮使用它來進行同步。理由以下:
synchronized
是Java
語法層面的同步,足夠清晰簡單Lock
必須由程序員確保在finally
塊中釋放鎖,而synchronized
能夠由JVM
確保鎖的自動釋放
- 測試並設置
- 獲取並增長
- 交換
- 比較並交換(
CAS
)- 加載連接 / 條件存儲
A.可重入代碼/ 純代碼
- 含義:可在代碼執行的任什麼時候刻中斷它去執行另一段代碼,當控制權返回後原來的程序並不會出現任何錯誤
- 共同特徵:不依賴存儲在堆上的數據和公用的系統資源、用到的狀態量都由參數中傳入、不調用非可重入的方法
- 斷定依據:若是一個方法,它的返回結果是可預測的,只要輸入相同的數據就都能返回相同的結果,就知足可重入性
- 注意:知足可重入性的代碼必定是線程安全的,反之,知足線程安全的代碼不必定是可重入的
B.線程本地存儲
- 含義:把共享數據的可見範圍限制在同一個線程以內,無須同步就能保證線程之間不出現數據爭用的問題
- 想詳細瞭解
ThreadLocal
的讀者,能夠看下筆者以前寫的一篇文章:進階之路 | 奇妙的 Handler 之旅
解決併發的正確性以後,爲了能在線程之間更『高效』地共享數據、解決競爭問題、提升程序的執行效率,下面介紹五種鎖優化技術
注意:自旋等待不能代替阻塞,它雖然能避免線程切換的開銷,但會佔用處理器時間,所以自旋等待的時間必需要有必定的限度,若是自旋超過了限定的次數(默認
10
次)仍未成功獲鎖,就須要掛線程了
- 若是對於某個鎖,自旋等待剛剛成功得到,且持有鎖的線程正在運行中,那麼虛擬機極可能容許自旋等待的時間更久點
- 若是對於某個鎖,自旋不多成功得到過,那麼極可能之後將省略自旋等待這個鎖,避免浪費處理器資源
首先先理解
HotSpot
虛擬機的對象頭的內存佈局:分爲兩部分
- 第一部分用於存儲對象自身的運行時數據,這部分被稱爲
Mark Word
,是實現輕量級鎖和偏向鎖的關鍵。如哈希碼、GC
分代年齡等- 另一部分用於存儲指向方法區對象類型數據的指針,若是是數組對象還會有一個額外的部分用於存儲數組長度
加鎖過程:
1.代碼進入同步塊時,若是同步對象未被鎖定(鎖標誌位爲 01
),虛擬機會在當前線程的棧幀中創建一個名爲 Lock Record
的空間,用於存儲鎖對象 Mark Word
的拷貝。以下圖
2.以後虛擬機會嘗試用 CAS
操做將對象的 Mark Word
更新爲指向 Lock Record
的指針。若更新動做成功,那麼當前線程就擁有了該對象的鎖,且對象 Mark Word
的鎖標誌位變爲 00
,即處於輕量級鎖定狀態;反之,虛擬機會先檢查對象的 Mark Word
是否指向當前線程的棧幀,如果,則當前線程已有該對象的鎖,可直接進入同步塊繼續執行,不然說明改對象已被其餘線程搶佔。以下圖:
另外,若是有兩條以上的線程爭用同一個鎖,那輕量級鎖就再也不有效,要膨脹爲重量級鎖,鎖標誌位變爲
10
,Mark Word
中存儲的就是指向重量級鎖的指針,後面等待鎖的線程也要進入阻塞狀態
解鎖過程:若對象的 Mark Word
仍指向着線程的 Lock Record
,就用 CAS
操做把對象當前的 Mark Word
和線程中複製的 Displaced Mark Word
替換回來。若替換成功,那麼就完成了整個同步過程;反之,說明有其餘線程嘗試獲取該鎖,那麼就要在釋放鎖的同時喚醒被掛起的線程
優勢:由於對於絕大部分的鎖,在整個同步週期內都是不存在競爭的,因此輕量級鎖經過使用 CAS
操做消除同步使用的互斥量
自旋鎖和輕量級鎖的關係:
- 自旋鎖是爲了減小線程掛起次數
- 輕量級鎖是在加鎖的時候,如何使用一種更高效的方式來加鎖
Q:處於輕量級鎖狀態時,會不會使用自旋鎖這個競爭機制
A:線程首先會經過
CAS
獲取鎖,失敗後經過自旋鎖來嘗試獲取鎖,再失敗鎖就膨脹爲重量級鎖。因此輕量級鎖狀態下可能會有自旋鎖的參與(CAS
將對象頭的標記指向鎖記錄指針失敗的時候)
若是說輕量級鎖是在無競爭的狀況下使用
CAS
去消除同步使用的互斥量那偏向鎖就是在無競爭狀況下把整個同步都消除掉
含義:偏向鎖會偏向於第一個得到它的線程,若是在後面的執行中該鎖沒有被其餘的線程獲取,則持有偏向鎖的線程將永遠不須要再進行同步
加鎖過程:啓用偏向鎖的鎖對象在第一次被線程獲取時,Mark Word
的鎖標誌位會被設置爲 01
,即偏向模式,同時使用 CAS
操做把獲取到這個鎖的線程 ID
記錄在對象的 Mark Word
中。若操做成功,持有偏向鎖的線程之後每次進入這個鎖相關的同步塊時均可再也不進行任何同步操做
解鎖過程:當有另外的線程去嘗試獲取這個鎖時,根據鎖對象目前是否處於被鎖定的狀態,撤銷偏向後恢復到未鎖定 01
或輕量級鎖定 00
的狀態,後續的同步操做就如輕量級鎖執行過程。以下圖:
優勢:可提升帶有同步但無競爭的程序性能,但若程序中大多數鎖總被多個線程訪問,此模式就不必了
可以寫出高性能、高伸縮性的併發程序是一門藝術,而瞭解併發在底層是如何實現的,則是掌握這門藝術的前提,也是成長爲高級程序員的必備知識!
加油吧!騷年!以夢爲馬,不負韶華!
若是文章對您有一點幫助的話,但願您能點一下贊,您的點贊,是我前進的動力
本文參考連接: