Java 5為了加強內置鎖的功能,引入了可重入鎖(ReentrantLock)。在此之前“synchronized”和“volatile”是實現並發的方式。
Synchronized關鍵字使用內置鎖(intrinsic lock)或者稱作監視鎖(monitor lock)。每一個Java對象都有一個內置鎖與之相關聯。無論什么時候,當一個線程嘗試去訪問一個synchronized代碼塊或者synchronized方法的時候,線程都需要首先獲取到對象關聯的內置鎖。對於static方法,線程獲取的是類對象的內置鎖。
內置鎖機制使得代碼的書寫非常整潔,並且大部分場合下功能也夠用。所以為什么我們需要額外的去顯式的創建鎖?讓我們討論一下。
內置鎖機制在功能上有一些限制:
- 不能中斷(interrupt)一個正在等待獲取鎖的線程
- 不能測試鎖是否空閑從而不用一直等待鎖
- 不能實現非代碼塊結構的加鎖與釋放鎖。因為內置鎖必須在和獲取鎖相同的代碼塊釋放鎖。
除此之外,ReentrantLock還支持對鎖的測試,支持既可以被中斷又可以設置超時。ReentrantLock還可以設置公平原則,允許更靈活的線程調度。
讓我們看一下實現了Lock類的ReentrantLock類中的部分方法:
讓我們理解一下如何使用上面的分析結果,看看我們會得到什么好處。
輪詢和設置了超時的對鎖的獲取
讓我們看一段代碼的例子:
在上面的方法中,當兩個線程A和B幾乎在同時轉賬(transfer money)的時候有可能會發生死鎖。
有可能線程A已經獲取了acc1對象的鎖並且正在等待獲取acc2對象的鎖,與此同時,線程B已經獲取了acc2對象的鎖並且正在等待獲取acc1的鎖。這將會導致死鎖,程序不得不需要重啟!!
然而有一個方法可以避免這種情況:就是所謂的按相同的順序獲取鎖。我個人覺得這個實現起來比較困難。
一個更簡便的實現方式是用ReentrantLock的tryLock()方法。這種方法稱為“輪詢式可超時的鎖獲取”。即便你不能夠獲取所有必須的鎖,它也可以使你重新獲得對程序的控制,釋放部分已經獲取的鎖,然后重新嘗試。
因此,我們將會使用trylock()來獲取兩個鎖,如果我們不能獲取到兩個鎖,可以釋放已經獲取到的一個,然后重試。
這里我們實現了一個支持超時的加鎖機制,所以,如果鎖在指定的時間段內不能被獲取到,方法將會返回失敗(false),優雅的退出。
獲取鎖可以被中斷
獲取鎖可以被中斷使得鎖可以使用在可以取消的操作上。
lockInterruptibly()方法使得我們可以嘗試去獲取鎖但是保留線程可以被中斷的能力。基本的意思是這個方法使得線程可以立即響應從其它線程發過來的中斷信號。這在當我們想要發送中斷信號到所有等待鎖的線程時會很有用。
讓我們看一個例子,假設我們有一個共享的方法來發送消息,希望如果有其他的線程請求中斷,那么正在發送的線程應當釋放鎖並且退出或者停止正在進行的操作以取消當前的任務。
帶超時的tryLock(long time, TimeUnit unit)方法也是可以響應中斷的。
非代碼塊加鎖
內置鎖的獲取和釋放是以代碼塊的結構出現的,即鎖總是在被獲取的同一個代碼塊被釋放,不管程序邏輯如何。
外置鎖提供了更加顯式的控制。在一些哈希容器和鏈表中使用到了外置鎖。
公平性
ReentrantLock的構造方法可以設置是否使用公平原則:創建一個公平鎖或者非公平鎖。使用公平鎖的線程們將會以他們請求獲取鎖的順序得到鎖,而非公平鎖允許線程不按請求順序獲取鎖,這稱作“闖入”(當鎖空閑時,打破隊列順序去獲取鎖)。
公平鎖因為會涉及到線程的掛起和恢復所以有很大的性能方面的代價。可能會有以下的情況:從一個掛起的線程被恢復到它開始實際執行會有嚴重的延時。讓我們看一個情形:
A -> 持有鎖
B -> 請求鎖,等待A釋放鎖,然后進入了掛起狀態
C -> 請求鎖,同時A釋放了鎖,此時C並沒有進入掛起狀態
由於C並沒有處於掛起狀態,所以它是有機會先去獲取A釋放的鎖,完成工作,然后甚至在線程B被喚醒之前就釋放鎖的。因此,這種情形下,非公平鎖具有非常大的性能上的優勢。
結篇
內置鎖和外置鎖的內部實現機制是一樣的,所以性能的提升是主觀上的。它依賴於我們上面討論的具體情況。外置鎖提供了對死鎖,線程飢餓等問題的顯示的處理方式。
作者公眾號(碼年)掃碼關注:
英文原文:
https://www.javacodegeeks.com/2013/11/what-are-reentrant-locks.html