鎖在計(jì)算機(jī)系統(tǒng)中是一個(gè)很常見的概念,無論是在一些編程語言中、關(guān)系型數(shù)據(jù)庫系統(tǒng)、內(nèi)存數(shù)據(jù)庫系統(tǒng)中,還是在一些分布式系統(tǒng)中都有鎖的影子。因此,鎖的類別、特性、功用也非常的豐富。今天我們就重點(diǎn)對(duì)java語言中常用的鎖做一個(gè)簡(jiǎn)要的介紹。
synchronized內(nèi)置鎖
為了對(duì)鎖有個(gè)基本的概念有個(gè)簡(jiǎn)單認(rèn)識(shí),我先從java的內(nèi)置鎖synchronized鎖來了解鎖的幾個(gè)基本特性??偨Y(jié)起來,synchronized鎖的有如下幾個(gè)基礎(chǔ)語義特性:
-
可見性
可見性是一種復(fù)雜的屬性。在單線程環(huán)境中,我們向某個(gè)變量寫入值,在沒有其他的寫入操作的情況下,我們?cè)俅巫x取該變量,一定能夠獲得相同的值,這是任何一門語言都要保證的,非常自然。然而,在多線程的情況下,在讀操作和寫操作在不同的線程中執(zhí)行時(shí),情況卻不一定。如果不加任何鎖或同步機(jī)制,很可能讀取到不一致的值。而內(nèi)置的synchronized鎖的使用卻可以保證線程間共享變量數(shù)據(jù)的一致性:擁有同一把鎖的多個(gè)線程讀取到的數(shù)據(jù)總是一致的。從而保證某個(gè)線性可以以一種可預(yù)測(cè)的方式來查看另一個(gè)線程的執(zhí)行結(jié)果。
-
原子性
原子性相對(duì)來說比較好理解,原子性是指程序中一系列指令的執(zhí)行不被中斷和影響,就保證了程序的原子性。怎么理解呢,比如下面一段程序:
i++; // 對(duì)事先定義好的int型變量i做自增操作上面的一行代碼非常簡(jiǎn)單,卻包含了三個(gè)操作:讀取
i變量、對(duì)變量i做+1的計(jì)算操作、將計(jì)算結(jié)果存儲(chǔ)到變量i中。如果這個(gè)”讀取-計(jì)算-寫入“的一系列操作不能保證原子性,也就是在這些操作執(zhí)行的過程中,i變量被其他的線程修改了,我們是很可能無法得到正確的結(jié)果的。這也正好是鎖的用武之地,保證程序的執(zhí)行的原子性。 -
互斥性
互斥性是指同時(shí)只能由一個(gè)持有鎖的線程在執(zhí)行中。正式因?yàn)閟ynchronized鎖的互斥性,才實(shí)現(xiàn)了上面介紹的可見性和原子性兩個(gè)特性。當(dāng)然,也不是所有鎖都是互斥的,比如ReaWriteLock中的兩個(gè)讀鎖就是非互斥的。
-
可重入性
可重入性,好像與互斥性矛盾,其實(shí)是合理的??芍厝胄允侵?,獲得鎖的線程可以重復(fù)的進(jìn)入同一個(gè)鎖保護(hù)的代碼塊或方法。這意味著synchronized鎖的粒度是線程,而不是方法調(diào)用。因此,可重入性也是非常有必要的,否則很可能會(huì)出現(xiàn)自己等待自己釋放鎖的情況,這也算是一種死鎖吧。
Lock鎖
在java的java.util.concurrent.locks包中還為我們提供了一種顯示的加鎖方式。其中Lock接口定義了Lock基本鎖的操作。Lock的最常用的實(shí)現(xiàn)是ReentrantLock。ReentrantLock的實(shí)現(xiàn)同樣保證了與內(nèi)置的synchronized鎖的可見性、原子性、互斥性和可重入性。那么為什么有了內(nèi)置鎖,又提供了一種新的加鎖機(jī)制呢?其實(shí)在大多數(shù)情況下,內(nèi)置的鎖已經(jīng)很好的滿足了我們的需求,但是在功能上顯示的Lock鎖有著更豐富的特性。Lock為我們提供了一種可輪詢的、定時(shí)的以及可中斷的鎖的操作。
可輪詢和可定時(shí)
Lock接口為我們提供了兩個(gè)方法分別為:
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
與內(nèi)置的鎖相比,tryLock方法有著更完善的錯(cuò)誤恢復(fù)機(jī)制。使用內(nèi)置鎖時(shí),防止死鎖的唯一方法就是避免出現(xiàn)死鎖,一旦出現(xiàn)死鎖,恢復(fù)程序的方法就是重啟程序。而利用可輪詢鎖和可定時(shí)的鎖,我們可以有更友好的方式避免死鎖的發(fā)生。利用tryLock()方法,我們可以通過輪詢的方式來獲取鎖,如果獲取第二個(gè)鎖失敗,我們可以做回退操作——釋放并重新獲取第一個(gè)鎖,從而避免死鎖。或者我們使用可定時(shí)的鎖,在等待鎖超過一定時(shí)限時(shí),放棄獲取鎖,從而可以采取其他更優(yōu)雅的方式退出競(jìng)爭(zhēng)。
可中斷的鎖
在使用synchronized內(nèi)置鎖時(shí),如果我們?cè)讷@取鎖的時(shí)候競(jìng)爭(zhēng)太激烈,很可能會(huì)程序的阻塞。如果我們想取消該鎖的獲取操作就很被動(dòng),因?yàn)閮?nèi)置鎖的阻塞是不可中斷的。而在Lock中boolean tryLock(long time, TimeUnit unit) throws InterruptedException和void lockInterruptibly() throws InterruptedException;方法都是可中斷的,這就為我們的取消操作提供了很高的靈活性。
公平鎖
由于公平的鎖在線程掛起和線程恢復(fù)時(shí)存在極大的開銷而事性能降低,因此,基于性能的考慮synchronized鎖是一種不公平的鎖。但如果我們需要一個(gè)先來后到的公平鎖,那么ReentrantLock為我們提供了一種構(gòu)建公平鎖的方式,如下:
Lock fairLock = new ReetrantLock(true); // 構(gòu)建公平鎖
非結(jié)構(gòu)化的加鎖
由于Lock的加鎖和釋放鎖都需要顯示的調(diào)用,因此,帶來另一個(gè)好處,就是我們可以在不同的代碼塊中進(jìn)行加鎖和釋放鎖操作。不過內(nèi)置的synchronized鎖的自動(dòng)釋放鎖操作也簡(jiǎn)化了代碼復(fù)雜度,避免了可能的編碼錯(cuò)誤,也是一種優(yōu)勢(shì)。
如何選擇
ReentrantLock提供了豐富的功能,在性能上也似乎略勝于內(nèi)置鎖,是不是我們應(yīng)該放棄內(nèi)置鎖而是全部使用顯示鎖呢?我的觀點(diǎn)是大可不必。其實(shí)內(nèi)置的synchronized鎖也有很大的優(yōu)勢(shì)。內(nèi)置鎖語法簡(jiǎn)潔緊湊,代碼易寫易讀利于維護(hù),同時(shí)也沒有忘記顯示釋放鎖的風(fēng)險(xiǎn)。只有在內(nèi)置的synchronized鎖無法滿足我們的一些高級(jí)需求的時(shí)候再考慮使用ReentrantLock吧,比如可定時(shí)、可輪詢、可中斷、公平鎖以及非結(jié)構(gòu)化的鎖。
volatile
提到synchronized,也就不得不提一下java中的volatile。volatile是java中提供的一種相比synchronized稍弱的同步機(jī)制。用volatile修飾的變量,可以確保該變量的值能夠及時(shí)的被其他線程感知到,即保證變量的可見性。當(dāng)我們將一個(gè)變量聲明為volatile類型后,編譯器與運(yùn)行時(shí)將不會(huì)對(duì)該變量的操作與其他內(nèi)存操作重排序,也不會(huì)將該變量緩存到寄存器或其他處理器不可見的地方,因此讀取volatile類型的變量總是能夠獲得最新寫入的值。由于在訪問volatile變量時(shí)不會(huì)執(zhí)行加鎖操作,所以線程也就不會(huì)阻塞,相比synchronized更輕量級(jí)、更高性能地實(shí)現(xiàn)了變量的可見性。然而,volatile修飾的變量并不能保證對(duì)該變量一系列操作的原子性,也就是說像上面提到的i++這種復(fù)合操作中,僅僅將變量用volatile修飾,是無法保證操作的線程安全的。