本篇文章給大家?guī)?lái)了關(guān)于java的相關(guān)知識(shí),其中主要介紹了關(guān)于鎖的相關(guān)內(nèi)容,包括了樂(lè)觀鎖、悲觀鎖、獨(dú)占鎖、共享鎖等等,下面一起來(lái)看一下,希望對(duì)大家有幫助。
推薦學(xué)習(xí):《java視頻教程》
樂(lè)觀鎖和悲觀鎖
悲觀鎖
悲觀鎖
對(duì)應(yīng)于生活中悲觀的人,悲觀的人總是想著事情往壞的方向發(fā)展。
舉個(gè)生活中的例子,假設(shè)廁所只有一個(gè)坑位了,悲觀鎖上廁所會(huì)第一時(shí)間把門(mén)反鎖上,這樣其他人上廁所只能在門(mén)外等候,這種狀態(tài)就是「阻塞」了。
回到代碼世界中,一個(gè)共享數(shù)據(jù)加了悲觀鎖,那線(xiàn)程每次想操作這個(gè)數(shù)據(jù)前都會(huì)假設(shè)其他線(xiàn)程也可能會(huì)操作這個(gè)數(shù)據(jù),所以每次操作前都會(huì)上鎖,這樣其他線(xiàn)程想操作這個(gè)數(shù)據(jù)拿不到鎖只能阻塞了。
在 Java 語(yǔ)言中 synchronized
和 ReentrantLock
等就是典型的悲觀鎖,還有一些使用了 synchronized 關(guān)鍵字的容器類(lèi)如 HashTable
等也是悲觀鎖的應(yīng)用。
樂(lè)觀鎖
樂(lè)觀鎖
對(duì)應(yīng)于生活中樂(lè)觀的人,樂(lè)觀的人總是想著事情往好的方向發(fā)展。
舉個(gè)生活中的例子,假設(shè)廁所只有一個(gè)坑位了,樂(lè)觀鎖認(rèn)為:這荒郊野外的,又沒(méi)有什么人,不會(huì)有人搶我坑位的,每次關(guān)門(mén)上鎖多浪費(fèi)時(shí)間,還是不加鎖好了。你看樂(lè)觀鎖就是天生樂(lè)觀!
回到代碼世界中,樂(lè)觀鎖操作數(shù)據(jù)時(shí)不會(huì)上鎖,在更新的時(shí)候會(huì)判斷一下在此期間是否有其他線(xiàn)程去更新這個(gè)數(shù)據(jù)。
樂(lè)觀鎖可以使用版本號(hào)機(jī)制
和CAS算法
實(shí)現(xiàn)。在 Java 語(yǔ)言中 java.util.concurrent.atomic
包下的原子類(lèi)就是使用CAS 樂(lè)觀鎖實(shí)現(xiàn)的。
兩種鎖的使用場(chǎng)景
悲觀鎖和樂(lè)觀鎖沒(méi)有孰優(yōu)孰劣,有其各自適應(yīng)的場(chǎng)景。
樂(lè)觀鎖適用于寫(xiě)比較少(沖突比較?。┑膱?chǎng)景,因?yàn)椴挥蒙湘i、釋放鎖,省去了鎖的開(kāi)銷(xiāo),從而提升了吞吐量。
如果是寫(xiě)多讀少的場(chǎng)景,即沖突比較嚴(yán)重,線(xiàn)程間競(jìng)爭(zhēng)激勵(lì),使用樂(lè)觀鎖就是導(dǎo)致線(xiàn)程不斷進(jìn)行重試,這樣可能還降低了性能,這種場(chǎng)景下使用悲觀鎖就比較合適。
獨(dú)占鎖和共享鎖
獨(dú)占鎖
獨(dú)占鎖
是指鎖一次只能被一個(gè)線(xiàn)程所持有。如果一個(gè)線(xiàn)程對(duì)數(shù)據(jù)加上排他鎖后,那么其他線(xiàn)程不能再對(duì)該數(shù)據(jù)加任何類(lèi)型的鎖。獲得獨(dú)占鎖的線(xiàn)程即能讀數(shù)據(jù)又能修改數(shù)據(jù)。
JDK中的synchronized
和java.util.concurrent(JUC)
包中Lock的實(shí)現(xiàn)類(lèi)就是獨(dú)占鎖。
共享鎖
共享鎖
是指鎖可被多個(gè)線(xiàn)程所持有。如果一個(gè)線(xiàn)程對(duì)數(shù)據(jù)加上共享鎖后,那么其他線(xiàn)程只能對(duì)數(shù)據(jù)再加共享鎖,不能加獨(dú)占鎖。獲得共享鎖的線(xiàn)程只能讀數(shù)據(jù),不能修改數(shù)據(jù)。
在 JDK 中 ReentrantReadWriteLock
就是一種共享鎖。
互斥鎖和讀寫(xiě)鎖
互斥鎖
互斥鎖
是獨(dú)占鎖的一種常規(guī)實(shí)現(xiàn),是指某一資源同時(shí)只允許一個(gè)訪(fǎng)問(wèn)者對(duì)其進(jìn)行訪(fǎng)問(wèn),具有唯一性和排它性。
互斥鎖一次只能一個(gè)線(xiàn)程擁有互斥鎖,其他線(xiàn)程只有等待。
讀寫(xiě)鎖
讀寫(xiě)鎖
是共享鎖的一種具體實(shí)現(xiàn)。讀寫(xiě)鎖管理一組鎖,一個(gè)是只讀的鎖,一個(gè)是寫(xiě)鎖。
讀鎖可以在沒(méi)有寫(xiě)鎖的時(shí)候被多個(gè)線(xiàn)程同時(shí)持有,而寫(xiě)鎖是獨(dú)占的。寫(xiě)鎖的優(yōu)先級(jí)要高于讀鎖,一個(gè)獲得了讀鎖的線(xiàn)程必須能看到前一個(gè)釋放的寫(xiě)鎖所更新的內(nèi)容。
讀寫(xiě)鎖相比于互斥鎖并發(fā)程度更高,每次只有一個(gè)寫(xiě)線(xiàn)程,但是同時(shí)可以有多個(gè)線(xiàn)程并發(fā)讀。
在 JDK 中定義了一個(gè)讀寫(xiě)鎖的接口:ReadWriteLock
public interface ReadWriteLock { /** * 獲取讀鎖 */ Lock readLock(); /** * 獲取寫(xiě)鎖 */ Lock writeLock(); }
ReentrantReadWriteLock
實(shí)現(xiàn)了ReadWriteLock
接口,具體實(shí)現(xiàn)這里不展開(kāi),后續(xù)會(huì)深入源碼解析。
公平鎖和非公平鎖
公平鎖
公平鎖
是指多個(gè)線(xiàn)程按照申請(qǐng)鎖的順序來(lái)獲取鎖,這里類(lèi)似排隊(duì)買(mǎi)票,先來(lái)的人先買(mǎi),后來(lái)的人在隊(duì)尾排著,這是公平的。
在 java 中可以通過(guò)構(gòu)造函數(shù)初始化公平鎖
/** * 創(chuàng)建一個(gè)可重入鎖,true 表示公平鎖,false 表示非公平鎖。默認(rèn)非公平鎖 */ Lock lock = new ReentrantLock(true);
非公平鎖
非公平鎖
是指多個(gè)線(xiàn)程獲取鎖的順序并不是按照申請(qǐng)鎖的順序,有可能后申請(qǐng)的線(xiàn)程比先申請(qǐng)的線(xiàn)程優(yōu)先獲取鎖,在高并發(fā)環(huán)境下,有可能造成優(yōu)先級(jí)翻轉(zhuǎn),或者饑餓的狀態(tài)(某個(gè)線(xiàn)程一直得不到鎖)。
在 java 中 synchronized 關(guān)鍵字是非公平鎖,ReentrantLock默認(rèn)也是非公平鎖。
/** * 創(chuàng)建一個(gè)可重入鎖,true 表示公平鎖,false 表示非公平鎖。默認(rèn)非公平鎖 */ Lock lock = new ReentrantLock(false);
可重入鎖
可重入鎖
又稱(chēng)之為遞歸鎖
,是指同一個(gè)線(xiàn)程在外層方法獲取了鎖,在進(jìn)入內(nèi)層方法會(huì)自動(dòng)獲取鎖。
對(duì)于Java ReentrantLock而言, 他的名字就可以看出是一個(gè)可重入鎖。對(duì)于Synchronized而言,也是一個(gè)可重入鎖。
敲黑板:可重入鎖的一個(gè)好處是可一定程度避免死鎖。
以 synchronized 為例,看一下下面的代碼:
public synchronized void mehtodA() throws Exception{ // Do some magic tings mehtodB(); } public synchronized void mehtodB() throws Exception{ // Do some magic tings }
上面的代碼中 methodA 調(diào)用 methodB,如果一個(gè)線(xiàn)程調(diào)用methodA 已經(jīng)獲取了鎖再去調(diào)用 methodB 就不需要再次獲取鎖了,這就是可重入鎖的特性。如果不是可重入鎖的話(huà),mehtodB 可能不會(huì)被當(dāng)前線(xiàn)程執(zhí)行,可能造成死鎖。
自旋鎖
自旋鎖
是指線(xiàn)程在沒(méi)有獲得鎖時(shí)不是被直接掛起,而是執(zhí)行一個(gè)忙循環(huán),這個(gè)忙循環(huán)就是所謂的自旋。
自旋鎖的目的是為了減少線(xiàn)程被掛起的幾率,因?yàn)榫€(xiàn)程的掛起和喚醒也都是耗資源的操作。
如果鎖被另一個(gè)線(xiàn)程占用的時(shí)間比較長(zhǎng),即使自旋了之后當(dāng)前線(xiàn)程還是會(huì)被掛起,忙循環(huán)就會(huì)變成浪費(fèi)系統(tǒng)資源的操作,反而降低了整體性能。因此自旋鎖是不適應(yīng)鎖占用時(shí)間長(zhǎng)的并發(fā)情況的。
在 Java 中,AtomicInteger
類(lèi)有自旋的操作,我們看一下代碼:
public final int getAndAddInt(Object o, long offset, int delta) { int v; do { v = getIntVolatile(o, offset); } while (!compareAndSwapInt(o, offset, v, v + delta)); return v; }
CAS 操作如果失敗就會(huì)一直循環(huán)獲取當(dāng)前 value 值然后重試。
另外自適應(yīng)自旋鎖也需要了解一下。
在JDK1.6又引入了自適應(yīng)自旋,這個(gè)就比較智能了,自旋時(shí)間不再固定,由前一次在同一個(gè)鎖上的自旋時(shí)間以及鎖的擁有者的狀態(tài)來(lái)決定。如果虛擬機(jī)認(rèn)為這次自旋也很有可能再次成功那就會(huì)次序較多的時(shí)間,如果自旋很少成功,那以后可能就直接省略掉自旋過(guò)程,避免浪費(fèi)處理器資源。
分段鎖
分段鎖
是一種鎖的設(shè)計(jì),并不是具體的一種鎖。
分段鎖設(shè)計(jì)目的是將鎖的粒度進(jìn)一步細(xì)化,當(dāng)操作不需要更新整個(gè)數(shù)組的時(shí)候,就僅僅針對(duì)數(shù)組中的一項(xiàng)進(jìn)行加鎖操作。
在 Java 語(yǔ)言中 CurrentHashMap 底層就用了分段鎖,使用Segment,就可以進(jìn)行并發(fā)使用了。
鎖升級(jí)(無(wú)鎖|偏向鎖|輕量級(jí)鎖|重量級(jí)鎖)
JDK1.6 為了提升性能減少獲得鎖和釋放鎖所帶來(lái)的消耗,引入了4種鎖的狀態(tài):無(wú)鎖
、偏向鎖
、輕量級(jí)鎖
和重量級(jí)鎖
,它會(huì)隨著多線(xiàn)程的競(jìng)爭(zhēng)情況逐漸升級(jí),但不能降級(jí)。
無(wú)鎖
無(wú)鎖
狀態(tài)其實(shí)就是上面講的樂(lè)觀鎖,這里不再贅述。
偏向鎖
Java偏向鎖(Biased Locking)是指它會(huì)偏向于第一個(gè)訪(fǎng)問(wèn)鎖的線(xiàn)程,如果在運(yùn)行過(guò)程中,只有一個(gè)線(xiàn)程訪(fǎng)問(wèn)加鎖的資源,不存在多線(xiàn)程競(jìng)爭(zhēng)的情況,那么線(xiàn)程是不需要重復(fù)獲取鎖的,這種情況下,就會(huì)給線(xiàn)程加一個(gè)偏向鎖。
偏向鎖的實(shí)現(xiàn)是通過(guò)控制對(duì)象Mark Word
的標(biāo)志位來(lái)實(shí)現(xiàn)的,如果當(dāng)前是可偏向狀態(tài)
,需要進(jìn)一步判斷對(duì)象頭存儲(chǔ)的線(xiàn)程 ID 是否與當(dāng)前線(xiàn)程 ID 一致,如果一致直接進(jìn)入。
輕量級(jí)鎖
當(dāng)線(xiàn)程競(jìng)爭(zhēng)變得比較激烈時(shí),偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖
,輕量級(jí)鎖認(rèn)為雖然競(jìng)爭(zhēng)是存在的,但是理想情況下競(jìng)爭(zhēng)的程度很低,通過(guò)自旋方式
等待上一個(gè)線(xiàn)程釋放鎖。
重量級(jí)鎖
如果線(xiàn)程并發(fā)進(jìn)一步加劇,線(xiàn)程的自旋超過(guò)了一定次數(shù),或者一個(gè)線(xiàn)程持有鎖,一個(gè)線(xiàn)程在自旋,又來(lái)了第三個(gè)線(xiàn)程訪(fǎng)問(wèn)時(shí)(反正就是競(jìng)爭(zhēng)繼續(xù)加大了),輕量級(jí)鎖就會(huì)膨脹為重量級(jí)鎖
,重量級(jí)鎖會(huì)使除了此時(shí)擁有鎖的線(xiàn)程以外的線(xiàn)程都阻塞。
升級(jí)到重量級(jí)鎖其實(shí)就是互斥鎖了,一個(gè)線(xiàn)程拿到鎖,其余線(xiàn)程都會(huì)處于阻塞等待狀態(tài)。
在 Java 中,synchronized 關(guān)鍵字內(nèi)部實(shí)現(xiàn)原理就是鎖升級(jí)的過(guò)程:無(wú)鎖 –> 偏向鎖 –> 輕量級(jí)鎖 –> 重量級(jí)鎖。這一過(guò)程在后續(xù)講解 synchronized 關(guān)鍵字的原理時(shí)會(huì)詳細(xì)介紹。
鎖優(yōu)化技術(shù)(鎖粗化、鎖消除)
鎖粗化
鎖粗化
就是將多個(gè)同步塊的數(shù)量減少,并將單個(gè)同步塊的作用范圍擴(kuò)大,本質(zhì)上就是將多次上鎖、解鎖的請(qǐng)求合并為一次同步請(qǐng)求。
舉個(gè)例子,一個(gè)循環(huán)體中有一個(gè)代碼同步塊,每次循環(huán)都會(huì)執(zhí)行加鎖解鎖操作。
private static final Object LOCK = new Object(); for(int i = 0;i < 100; i++) { synchronized(LOCK){ // do some magic things } }
經(jīng)過(guò)鎖粗化
后就變成下面這個(gè)樣子了:
synchronized(LOCK){ for(int i = 0;i < 100; i++) { // do some magic things } }
鎖消除
鎖消除
是指虛擬機(jī)編譯器在運(yùn)行時(shí)檢測(cè)到了共享數(shù)據(jù)沒(méi)有競(jìng)爭(zhēng)的鎖,從而將這些鎖進(jìn)行消除。
舉個(gè)例子讓大家更好理解。
public String test(String s1, String s2){ StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append(s1); stringBuffer.append(s2); return stringBuffer.toString(); }
上面代碼中有一個(gè) test 方法,主要作用是將字符串 s1 和字符串 s2 串聯(lián)起來(lái)。
test 方法中三個(gè)變量s1, s2, stringBuffer, 它們都是局部變量,局部變量是在棧上的,棧是線(xiàn)程私有的,所以就算有多個(gè)線(xiàn)程訪(fǎng)問(wèn) test 方法也是線(xiàn)程安全的。
我們都知道 StringBuffer 是線(xiàn)程安全的類(lèi),append 方法是同步方法,但是 test 方法本來(lái)就是線(xiàn)程安全的,為了提升效率,虛擬機(jī)幫我們消除了這些同步鎖,這個(gè)過(guò)程就被稱(chēng)為鎖消除
。
StringBuffer.class // append 是同步方法 public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }
一張圖總結(jié):
Java 并發(fā)編程的知識(shí)非常多,同時(shí)也是 Java 面試的高頻考點(diǎn),面試官必問(wèn)的,需要學(xué)習(xí) Java 并發(fā)編程其他知識(shí)的小伙伴可以去下載『阿里師兄總結(jié)的Java知識(shí)筆記 總共 283 頁(yè),超級(jí)詳細(xì)』。
前面講了 Java 語(yǔ)言中各種各種的鎖,最后再通過(guò)六個(gè)問(wèn)題統(tǒng)一總結(jié)一下:
推薦學(xué)習(xí):《java視頻教程》