每日一例 | 多線程編程之可重入鎖

在目前web的開發(fā)大環(huán)境下,高并發(fā),高可用的應用場景越來越普遍,對我們的要求也越來越要求越高了,為了應對這樣超高的要求(比如多線程環(huán)境下的數據共享問題),我們必須掌握很多常用的技術方案,比如鎖(Lock)(就是在某個方法或資源上加鎖,確保同一時間段內只有我們可以訪問該資源),這樣才能寫出更可靠的應用程序,今天我們就一起來看下一個很常用的鎖——可重入鎖(ReentrantLock)。
在開始今天的內容之前,我們先考慮這樣一個場景:我們有一個審核業(yè)務,同一級的審核人員有兩個,但是業(yè)務只能審核一次,不能重復審核。

如上圖,如果整個審核方法不加鎖的情況下,很可能發(fā)生同一筆數據審核兩次的情況。因為審核過程會涉及多個步驟,假如第一個人員在查詢未審核數據后,進行業(yè)務審核(處在第三步),但是尚未提交審核結果,這時候第二個人進來,也是查了未審核數據(第二步),由于第一個人員未提交審核結果,這時候數據依然是未審核,然后第二個人開始審核,這時候第一個人提交了審核結果,然后緊接著第二個人提交審核結果。最后,審核結果就會變成兩條。
接下來,我們講的內容,就是為了解決這樣的額應用場景。
一個不加鎖的案例
在開始可重入鎖的介紹之前,我們先看一個和上面類似的例子,算是簡化版:
public class Example {
private static int i;
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 1, TimeUnit.MICROSECONDS, new ArrayBlockingQueue<Runnable>(100));
for (int j = 0; j < 1000; j++) {
Thread.sleep(10L);
final int finalJ = j;
executor.submit(() -> test(finalJ));
}
executor.shutdown();
}
public static void test(int j) {
System.out.println("==第" + j + "次調用==start");
i ++;
Thread.sleep(20L);
i ++;
System.out.println(i);
System.out.println("==第" + j + "次調用==end");
}
}
上面這段代碼其實就是模擬多線程共享數據(就是這里的i),并對數據進行操作的一個示例,運行結果可以很直觀的說明,不加鎖的情況下,在一個線程未執(zhí)行完方法之前,另一個方法也會進入方法執(zhí)行。按照我們代碼的邏輯,應該是先打印start,然后打印i的值,然后再打印end,但是實際情況卻并發(fā)如此,往往可能是這樣的:

上面的運行結果很直觀的說明,在第1995次未正常運行結束時,第1996次已經開始了,同樣在第1996次未運行完的時候,第1998次都開始了。而且不論你運行多少次,上面的結果都大同小異。
這時候,如果我們將代碼調整一下,加上鎖,看下會發(fā)生什么:
public class Example {
// 可重入鎖
private static final ReentrantLock mainLock = new ReentrantLock();
private static int i;
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 1, TimeUnit.MICROSECONDS, new ArrayBlockingQueue<Runnable>(100));
for (int j = 0; j < 1000; j++) {
Thread.sleep(10L);
final int finalJ = j;
executor.submit(() -> testLock(finalJ));
}
executor.shutdown();
}
public static void testLock(int j) {
final ReentrantLock reentrantLock = mainLock;
// 如果被其它線程占用鎖,會阻塞在此等待鎖釋放
reentrantLock.lock();
try {
System.out.println("==第" + j + "次調用==start");
i ++;
Thread.sleep(20L);
i ++;
System.out.println(i);
System.out.println("==第" + j + "次調用==end");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 執(zhí)行完之后必須釋放鎖
reentrantLock.unlock();
}
}
}
然后我們運行一下:

這時候,你會發(fā)現,無論你運行多少次,都是像上面這樣規(guī)整,也和我們的代碼邏輯是一致的,這其實就是加鎖的作用,目的就是為了控制資源的訪問秩序。
當然,上面的代碼其實還是存在問題的,因為在循環(huán)中使用線程池本身就是不合理的,當單個線程執(zhí)行時間較長,for中啟動前程前的業(yè)務響應比較快的時候(就是這里的Thread.sleep(10L);),所有的壓力都會到線程池上,會把線程池的資源耗盡,然后報如下錯誤:

這時候解決方法有兩個,一個就是人為增加線程啟動前的業(yè)務處理時間,這里就是增加睡眠時間,比如調整到Thread.sleep(20L);;另一個是提高線程中的業(yè)務處理效率,只要比前面的業(yè)務處理快就行,但是在實際業(yè)務中,這個是不可能的;最好的解決方法是重構業(yè)務邏輯,想辦法把for循環(huán)放進線程里面,我之前修復的異步線程問題就用的是這個方法。好了,下面開始理論方面的學習。
什么是可重入鎖
可重入鎖,顧名思義就是可以重復加鎖的一種鎖,它是指,線程可對同一把鎖進行重復加鎖,而不會被阻塞住,這樣可避免死鎖的產生。
加鎖的方式
它的加鎖方式有三種,分別是lock、trylock和trylock(long,TimeUnit)。上面我們加鎖的方法只是其中一種,也是最簡單的。
可以看到ReentrantLock的使用方式比較簡單,創(chuàng)建出一個ReentrantLock對象,通過lock()方法進行加鎖,使用unlock()方法進行釋放鎖操作。
使用lock來獲取鎖的話,如果鎖被其他線程持有,那么就會處于等待狀態(tài)。同時,需要我們去主動的調用``unlock`方法去釋放鎖,即使發(fā)生異常,它也不會主動釋放鎖,需要我們顯式的釋放。
使用trylock方法獲取鎖,是有返回值的,獲取成功返回true,獲取失敗返回false,不會一直處于等待狀態(tài)。
使用trylock(long,TimeUnit)指定時間參數來獲取鎖,在等待時間內獲取到鎖返回true,超時返回false。還可以調用lockInterruptibly方法去中斷鎖,如果線程正在等待獲取鎖,可以中斷線程的等待狀態(tài)。
總結
關于鎖這一塊,其實內容比較多,涉及的知識也比較雜,不僅包括java的synchronized、原子類、鎖等這些線程安全的知識,還包括數據的行級鎖、表級鎖等內容,如果是分布式應用,還需要考慮分布式鎖的實現,這里面還涉及了redis的知識,想要完全掌握還是難度很大的,但是隨著我們一點點的學習和應用,你慢慢會掌握很多常用的技術和解決方案,你會更清楚各種鎖和技術的應用場景,你會涉及出更優(yōu)秀的高并發(fā)高可用的系統(tǒng),為了實現這個目標,讓我們一起學習,一起遇見更好的自己,加油吧!
項目路徑:
https://github.com/Syske/example-everyday
本項目會每日更新,讓我們一起學習,一起進步,遇見更好的自己,加油呀
- END -