Java“鎖”記
內建鎖其實是相對顯示鎖來說的,說白了內建鎖就是synchronized
所代表Java原生鎖機制,Jdk5.0之後又引入了Lock
及其子類ReentrantLock
這樣一種新的鎖機制。從加鎖和記憶體語義上二者一樣,只不過後者添加了一些其他功能,可以實現諸如輪詢鎖、超時鎖和中斷鎖的功能。
public interface Lock { void lock(); void lockInterruptibly() throw InterruptedException; boolean tryLock(); boolean tryLock(long timeout, TimeUnit unit) throw InterruptedException; void unlock(); Condition newCondition(); }
如果內建鎖是一個Lock
的話,它只有lock()
和unlock()
方法。從鎖的基本屬性上說,內建鎖和顯示鎖都是可重入的,內建鎖是非公平的,顯示鎖還可以設定為公平的。
tryLock
和lock
的區別是前者獲得鎖返回true,獲取不到返回false,都是立馬返回,而後者如果獲取不到將會阻塞到那裡。
另外由於內建鎖是自動釋放,而顯示鎖必須手動釋放,這就形成了顯示鎖的呼叫模式如下面這樣:
Lock lock = ...; lock.lock(); try { // 邏輯 } finally { lock.unlock(); }
也就是鎖的釋放必須放在finally中,確保鎖可以釋放。
從ReentrantLock
衍生出來一個ReentrantReadWriteLock
,為啥要有讀寫鎖呢?其實是基於這樣的原則,讀寫和寫寫是會引起執行緒安全問題的,所以都需要同步,前者是因為可見性,後者是因為一致性,但是讀讀是不需要同步的,所以講讀寫拆分開來以提高效能。這就好比原來大家都排一個隊,現在拆成兩個隊,自然排隊等待的時間就短了。
閉鎖
閉鎖就像一個門,等待一個“事件”開門(結束狀態),在開門之前不允許任何人(執行緒)通過,在此之前大家只能在城門前面等待。只不過城門可以重複的開閉,閉鎖只是一次性的。
具體到Java中,閉鎖的實現就是CountDownLatch
,它可以用來實現等待某種條件滿足後才把執行緒放行的功能,比如資源就緒、服務啟動、某個操作執行等等。
訊號量
訊號量是用來控制同時訪問某個資源的特定數量,或者同時執行某個操作的數量,有點像地鐵中的限流。
從某種程度上講,鎖有點像一個二值的訊號量,也就是初始值為1的訊號量,不同之處是鎖是可重入的,訊號量不可。
柵欄
柵欄和閉鎖類似,它也能阻塞一組執行緒直到某個事件發生。區別在於柵欄要求執行緒都到達柵欄位置,才能繼續執行,即所謂的閉鎖等待的是事件,柵欄等待的是執行緒。如果對比現實中的例子,閉鎖猶如大家去登山,商議好早晨8點出發,無論人齊不齊,到8點大家就出發,而柵欄就類似於大家登一段就在一個歇息點等一等人,等人齊再往上登。
原子變數
原子變數實際上是一種樂觀鎖技術,即利用衝突檢測來判斷是否有來自其他執行緒的干擾,當進行修改操作時,先把變數的當前值current取出來,然後用一個原子的比較交換操作(CAS)對變數進行修改。有兩種情況:如果變數的當前值還等於current說明這中間沒有執行緒修改變數,修改變數值為新值;如果當前值不等於current了,說明中間有執行緒修改變數,重試。
以一個典型的count++為例,大家知道++這種操作實際上包括三步:
- 獲取count當前值current
- 當前值加一newvalue
- 將newvalue賦值給count
如果兩個執行緒同時修改count的值,假如兩個執行緒的時序如下:
=====1===========+1================= =========1===========+1=============
假設count的當前值為1,兩個執行緒分別進行了++的操作,最後的值為2,第一個++操作被“覆蓋”了。如果把上面2、3步換成一個CAS操作就不會發生上面的情況了,因為執行第二次操作時會拿count的舊值1和新值2對比,一對比發現不一樣,說明其他執行緒修改了變數,這時候第二個執行緒會進入下一次的CAS操作,重新獲取count值2,比較當前值2等於原來的值,修改為新值3。
原子變數作為一種非阻塞的鎖技術,適用在讀操作比較多、競爭不那麼激烈的場景,這適用於大部分的業務場景。但同時原子變數也有其侷限,原子鎖只能保證單一變數的執行緒安全。