多線程編程之可重入鎖

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

如上圖,如果整個審核方法不加鎖的情況下,很可能發(fā)生同一筆數(shù)據(jù)審核兩次的情況。因為審核過程會涉及多個步驟,假如第一個人員在查詢未審核數(shù)據(jù)后,進行業(yè)務(wù)審核(處在第三步),但是尚未提交審核結(jié)果,這時候第二個人進來,也是查了未審核數(shù)據(jù)(第二步),由于第一個人員未提交審核結(jié)果,這時候數(shù)據(jù)依然是未審核,然后第二個人開始審核,這時候第一個人提交了審核結(jié)果,然后緊接著第二個人提交審核結(jié)果。最后,審核結(jié)果就會變成兩條。
接下來,我們講的內(nèi)容,就是為了解決這樣的額應(yīng)用場景。
一個不加鎖的案例
在開始可重入鎖的介紹之前,我們先看一個和上面類似的例子,算是簡化版:
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(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?+?"次調(diào)用==start");
????????i?++;
????????Thread.sleep(20L);
????????i?++;
????????System.out.println(i);
????????System.out.println("==第"?+?j?+?"次調(diào)用==end");
????}
}
上面這段代碼其實就是模擬多線程共享數(shù)據(jù)(就是這里的i),并對數(shù)據(jù)進行操作的一個示例,運行結(jié)果可以很直觀的說明,不加鎖的情況下,在一個線程未執(zhí)行完方法之前,另一個方法也會進入方法執(zhí)行。按照我們代碼的邏輯,應(yīng)該是先打印start,然后打印i的值,然后再打印end,但是實際情況卻并發(fā)如此,往往可能是這樣的:

上面的運行結(jié)果很直觀的說明,在第1995次未正常運行結(jié)束時,第1996次已經(jīng)開始了,同樣在第1996次未運行完的時候,第1998次都開始了。而且不論你運行多少次,上面的結(jié)果都大同小異。
這時候,如果我們將代碼調(diào)整一下,加上鎖,看下會發(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(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?+?"次調(diào)用==start");
????????????i?++;
????????????Thread.sleep(20L);
????????????i?++;
????????????System.out.println(i);
????????????System.out.println("==第"?+?j?+?"次調(diào)用==end");
????????}?catch?(Exception?e)?{
????????????e.printStackTrace();
????????}?finally?{
????????????//?執(zhí)行完之后必須釋放鎖
????????????reentrantLock.unlock();
????????}
????}
}
然后我們運行一下:

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

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