? ? ? ?在計算機科學中,鎖或互斥(來自互斥)是一種同步機制,用于在多線程執(zhí)行環(huán)境中強制限制對資源的訪問。 鎖旨在實施互斥并發(fā)控制策略。鎖機制的引入就是為了解決多線程環(huán)境下結果不可預知的情況。
分類
- 建議鎖:常見的鎖大多都是建議鎖,它是指線程在訪問數(shù)據(jù)之前會通過獲取鎖來協(xié)助工作,此時也可以不通過獲取鎖來讀取相應數(shù)據(jù),一樣不會出現(xiàn)異常,只是結果可能無法預期。
- 強制鎖:還有一些系統(tǒng)會使用強制鎖,在這些系統(tǒng)中如果對鎖定的資源沒有進行授權而直接訪問,會直接拋出異常,導致讀取終端,并強制拋出異常。
? ? ? ?可以說鎖的存在就是為了應對多線程環(huán)境下臨界資源的并發(fā)訪問可能出現(xiàn)的問題。這些問題其實還是和計算機或者是程序所處的環(huán)境有直接關系,這里就不得不提一個概念--內(nèi)存模型。
Java內(nèi)存模型(JMM)
? ? ? ?Java程序本身也存在它的內(nèi)存模型,這個是跟jvm息息相關的,在了解它之前,先得了解以下計算機本身的物理內(nèi)存模型,然后通過它與JMM做類比,大致就能明白了。
計算機內(nèi)存模型
? ? ? ?我們知道,在計算機中,比較核心的是:CPU、內(nèi)存這兩個部分,至于其他的顯卡,主板之類主要是能提升整體的使用性能,但是計算機的核心還是計算,計算就離不開CPU,同時計算就會產(chǎn)生數(shù)據(jù),有了數(shù)據(jù)就離不開存儲,所以CPU+內(nèi)存處于核心地位。
? ? ? ?在早期計算機剛剛出現(xiàn)的時候,其實CPU和內(nèi)存的處理速度基本上沒有太大差距,所以CPU可以和內(nèi)存直接互連,內(nèi)存的存取效率也完全跟得上CPU的處理效率,但是隨著計算機的發(fā)展,CPU的速度越來越快,尤其還出現(xiàn)了著名的摩爾定律,但是與此同時內(nèi)存的發(fā)展遠遠跟不上CPU的發(fā)展,導致它們之間出現(xiàn)了難以逾越的鴻溝,此時就會面臨CPU的性能受限于內(nèi)存的性能,不能完全發(fā)揮CPU高速運轉的特性,所以出現(xiàn)了一級緩存(L1)。
? ? ? ?一級緩存的作用就是緩解內(nèi)存與CPU之間的性能差距,通常都是一級緩存內(nèi)部存儲一定量的內(nèi)存空間中的數(shù)據(jù)(一般都是高頻使用的數(shù)據(jù)),這樣CPU讀取或者寫出這些被緩存的數(shù)據(jù)時,可以無需與內(nèi)存打交道,直接通過一級緩存就行,一級緩存都內(nèi)置在CPU內(nèi)部并與CPU同速運行,可以有效的提高CPU的運行效率。一級緩存越大,CPU的運行效率越高,但受到CPU內(nèi)部結構的限制,一級緩存的容量都很小。
? ? ? ?然后因為CPU發(fā)展極其迅速,到了現(xiàn)在也會有二級緩存(L2)和三級緩存(L3)這種,它們之間:L1存儲的所有數(shù)據(jù)只是L2的一部分,同理L2存儲的所有數(shù)據(jù)也只是L3的一部分。
? ? ? ?看到過一個比喻,可能不太恰當,但是也是比較形象的:說緩存的層級就像一個公司一樣,創(chuàng)業(yè)初期,老板(CPU)與員工(內(nèi)存)可能沒有太大的差距,但是隨著公司規(guī)模的擴大,老板的成長速度遠遠超過了員工的成長速度,老板就無法對每個員工面面俱到,所以就出現(xiàn)了管理層,老板無需對每個員工都直接對接發(fā)布任務,直接對接管理層(L1),由管理層下發(fā)各個命令。同時隨著公司進一步擴大,可能會出現(xiàn)多級管理層。
? ? ? ?現(xiàn)代的計算機CPU可能不止一個,大多數(shù)的計算機都是多核,這就類似于公司里的合伙人一樣,公司規(guī)模較大之后,每個合伙人(CPU)都有自己的嫡系手下(各自的緩存層),所以對應的是多核計算機會有多個L1、L2緩存,分屬于不同的CPU核心,但是L3緩存是共享的,就像合伙人公司底層員工一樣,所有合伙人共享整個員工資源。
Java內(nèi)存模型
? ? ? ?類比計算機的內(nèi)存模型,Java也有一套自己的內(nèi)存模型,它沒有計算機那么多緩存層級,Java的內(nèi)存模型只有類似于“一級緩存”的模型。在Java中,一般關于對象的數(shù)據(jù)(如:實例域、靜態(tài)域和數(shù)組元素等)存在于堆內(nèi)存中,這塊區(qū)域是Java內(nèi)部所有線程共享的,這里稱之為公共空間;每個線程在運行階段還會有各個線程自己的私有空間(類比于一級緩存),其他線程無法觸及到該空間區(qū)域,這里稱之為線程的工作空間。
? ? ? ? 假設現(xiàn)在有A和B之間有個變量
x要實現(xiàn)通訊,線程A從公共空間讀取x的值,改變x的值后,將x重新刷回到主內(nèi)存去;然后線程B從主內(nèi)存區(qū)域讀取 ,將x拷貝一份到B的工作空間,而對于x的刷入公共空間以及將數(shù)據(jù)拷貝到工作空間這個過程就是JMM(Java內(nèi)存模型)在控制。
? ? ? ?JDK1.5之后Java的內(nèi)存模型其實經(jīng)過了一次重大的升級(SR -133內(nèi)存模型),在此之前的內(nèi)存模型不再深入了解,只要知道它存在很多問題,甚至在一些條件下final都可以被修改,所以說老的內(nèi)存模型存在很大的漏洞。
導致并發(fā)問題的原因
? ? ? ?因為多線程并發(fā)操作,結合前面的JMM,我們可以發(fā)現(xiàn),如果線程A在處理x變量的時候,如果需要對x進行++操作,同時線程B也在進行相同的操作,可能會出現(xiàn)的問題是:初始時x都是0,所以A的工作空間和B的工作空間x都是0,都進行了++操作,最后線程A得到x = 1,線程B也得到x = 1,這顯然不符合理想結果(理想結果是x = 2)。這就帶來了并發(fā)問題。
一般并發(fā)問題都離不開三個概念:重排序、可見性和原子性。
重排序
? ? ? ?在實際的編碼過程中,開發(fā)人員寫出來的程序代碼,它的執(zhí)行順序和具體編譯后執(zhí)行的順序可能不太一樣,這個主要是編譯器以及計算機內(nèi)部有一個優(yōu)化執(zhí)行效果的過程,用于提高程序執(zhí)行性能,它有一個原則:如果兩個步驟之間不存在依賴的關系,同時在不改變單線程程序語義的情況下,允許重新安排指令的執(zhí)行順序
一般有三種重排序:
- 編譯器優(yōu)化的重排序:編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執(zhí)行順序。
- 指令級并行的重排序:現(xiàn)代處理器采用了指令級并行技術(Instruction-Level Parallelism, ILP)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對應機器指令的執(zhí)行順序。
- 內(nèi)存系統(tǒng)的重排序:由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲操作看上去可能是在亂序執(zhí)行。
? ? ? ?上面的后兩種重排序屬于處理器級別的重排序。同時重排序也是基于一定的規(guī)則的,SR-133模型主要就是為了闡述基于內(nèi)存的操作和它們之間的可見性原則,即:如果一個操作執(zhí)行的結果需要對另一個操作可見,那么這兩個操作之間必須存在happens-before關系。
- 程序順序規(guī)則:一個線程中的每個操作,happens- before 于該線程中的任意后續(xù)操作。
- 監(jiān)視器鎖規(guī)則:對一個監(jiān)視器鎖的解鎖,happens- before 于隨后對這個監(jiān)視器鎖的加鎖。
- volatile變量規(guī)則:對一個volatile域的寫,happens- before 于任意后續(xù)對這個volatile域的讀。
- 傳遞性:如果A happens- before B,且B happens- before C,那么A happens- before C。
? ? ? ?注意:這里的happens-before僅僅只是要求前一個操作(執(zhí)行的結果)對后一個操作可見,并不要求前一個操作必須要在后一個操作之前執(zhí)行,這里我的理解是:如果存在step1和step2兩個步驟的操作,但是兩個操作互不影響,雖然step1 happens before step2,但是實際中如果發(fā)生重排序,可能會出現(xiàn)step2 先于step1執(zhí)行,因為這里的執(zhí)行結果與happens-before的結果一致,JMM就會認為它是合法的。
可見性
所謂可見性:就是指在多線程環(huán)境中,某個數(shù)據(jù)被其中一條線程修改了,能夠立即被其他線程感知到。
? ? ? ?volatile關鍵字主要用于修飾變量,在Java中的作用就相當于強制給代碼加了一個內(nèi)存屏障指令,被它修飾的變量,在線程執(zhí)行時,如果讀取該變量,會強制到主內(nèi)存中去讀取最新的值到工作空間中,而不是先到工作空間中區(qū)讀取線程緩存的值;對應的,如果對變量進行寫操作,會強制將更新后的變量刷新回主內(nèi)存中,保證線程間的可見性。
? ? ? ?在JMM的控制下,用volatile修飾的變量在一定程度上是不會進行重排序的,這也從微觀上對volatile字段進行了一致性的加強。但是volatile僅僅只是加了一個內(nèi)存屏障指令,也就是說對于數(shù)據(jù)在線程之間的可見性是可以保障的,比如線程t1對變量x的修改,其他線程在讀取x變量的值時,可以獲取到最新變化的值;可是它卻無法保障原子性,即所謂的:要么全執(zhí)行,要么全不執(zhí)行。
? ? ? ? 舉例:
x++操作,在指令層面大致可以分為三步,讀取x值,對讀取到的值加1,將加1后的值寫回x,現(xiàn)兩個線程同時對初始值為0的x執(zhí)行加加操作,各5000次之加加后,得到的結果一般會小于10000。歸根結底就是:只要是指令層面無法一條指令能夠完成的,如果沒有特殊手段,都會存在原子性問題。
? ? ? ?一般volatile主要用于相互之間不存在依賴關系的語句中,上面x++的例子之所以有問題,就是因為對x++的操作是與上一步操作的結果相依賴的。所以在并發(fā)環(huán)境下一般volatile用于修飾boolean變量,它的值只能是true或false,利用它的變化,來實現(xiàn)一些其他操作,因為它不會存在相互依賴的關系。
原子性
? ? ? ?原子性說的就是某一段執(zhí)行邏輯,具有不可拆分的特性,要么全部執(zhí)行,要么全部不執(zhí)行,不存在執(zhí)行到一半放棄CPU權限的情況。就像日常生活中銀行交易系統(tǒng)一樣,取錢和扣款必須是一個原子操作,不能存在只取錢不扣款的情況,或者是只扣款不取錢的情況。
? ? ? ?Java中可以用synchronized保證原子性和可見性,相比于volatile,synchronized的開銷就非常大了,但是它可以保證原子性和可見性。當線程對鎖釋放的時候,其內(nèi)存語義就是把該線程對應的本地內(nèi)存中的共享變量刷新到主內(nèi)存中。當線程獲取鎖時,JMM會把該線程對應的本地內(nèi)存置為無效。從而使得被監(jiān)視器保護的臨界區(qū)代碼必須要從主內(nèi)存中去讀取共享變量。
public class SynchronizedTest {
public synchronized void method1() {
System.out.println("synchronized method");
}
public void method2() {
synchronized (this) {
System.out.println("synchronized code block");
}
}
}
現(xiàn)在我們通過javap命令反編譯上面的代碼:
G:\study_workspace> javap -v -c SynchronizedTest.class
......//省略前面部分
public synchronized void method1();
descriptor: ()V
flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String synchronized method
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 4: 0
line 5: 8
public void method2();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #5 // String synchronized code block
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit
//......省略后面分
? ? ? ?反編譯SynchronizedTest類,我們可以看到Java編譯器為我們生成的字節(jié)碼。在對于method1和method2的處理上稍有不同。也就是說。JVM對于同步方法和同步代碼塊的處理方式不同。對于同步方法,JVM采用ACC_SYNCHRONIZED標記符來實現(xiàn)同步。 對于同步代碼塊。JVM采用monitorenter、monitorexit兩個指令來實現(xiàn)同步。
? ? ? ?這里的monitorenter和monitorexit,在大學操作系統(tǒng)上其實是有介紹的,在操作系統(tǒng)層面,它其實叫管程(Monitors),也稱為監(jiān)視器,是一種程序結構,結構內(nèi)的多個子程序(對象或模塊)形成的多個工作線程互斥訪問共享資源。這些共享資源一般是硬件設備或一群變量。管程實現(xiàn)了在一個時間點,最多只有一個線程在執(zhí)行管程的某個子程序。與那些通過修改數(shù)據(jù)結構實現(xiàn)互斥訪問的并發(fā)程序設計相比,管程實現(xiàn)很大程度上簡化了程序設計。 管程提供了一種機制,線程可以臨時放棄互斥訪問,等待某些條件得到滿足后,重新獲得執(zhí)行權恢復它的互斥訪問。
? ? ? ?我們可以把監(jiān)視器理解為包含一個特殊的房間的建筑物,這個特殊房間同一時刻只能有一個客人(線程)。如果一個顧客想要進入這個特殊的房間,他首先需要在走廊(Entry Set)排隊等待。調度器將基于某個標準(比如 FIFO)來選擇排隊的客戶進入房間。如果,因為某些原因,該客戶客戶暫時因為其他事情無法脫身(線程被掛起),那么他將被送到另外一間專門用來等待的房間(Wait Set),這個房間的可以在稍后再次進入那間特殊的房間。如上面所說,這個建筑屋中一共有三個場所。
image.png
有序性
? ? ? ?有序性即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。除了引入了時間片以外,由于處理器優(yōu)化和指令重排等,CPU還可能對輸入代碼進行亂序執(zhí)行,比如load->add->save 有可能被優(yōu)化成load->save->add 。這就是可能存在有序性問題。這里需要注意的是,synchronized是無法禁止指令重排和處理器優(yōu)化的。也就是說,synchronized無法避免上述提到的問題。
as-if-serial語義:不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執(zhí)行結果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語義。
? ? ? ?as-if-serial語義保證了單線程中,指令重排是有一定的限制的,而只要編譯器和處理器都遵守了這個語義,那么就可以認為單線程程序是按照順序執(zhí)行的。當然,實際上還是有重排的,只不過我們無須關心這種重排的干擾。
? ? ? ?由于synchronized修飾的代碼,同一時間只能被同一線程訪問。那么也就是單線程執(zhí)行的。所以,可以保證其有序性。
鎖優(yōu)化
? ? ? ?synchronized其實是借助Monitor實現(xiàn)的,Monitor是基于C++實現(xiàn)的,在加鎖時會調用objectMonitor的enter()方法,解鎖的時候會調用exit方法。事實上,只有在JDK1.6之前,synchronized的實現(xiàn)才會直接調用ObjectMonitor的enter()和exit(),這種鎖被稱之為重量級鎖。
? ? ? ?在JDK1.6中出現(xiàn)對鎖進行了很多的優(yōu)化,進而出現(xiàn)輕量級鎖,偏向鎖,鎖消除,適應性自旋鎖,鎖粗化(自旋鎖在1.4就有,只不過默認的是關閉的,jdk1.6是默認開啟的),這些操作都是為了在線程之間更高效的共享數(shù)據(jù) ,解決競爭問題。
? ? ? ?JDK 1.5 到 JDK 1.6的一個重要改進就是高效并發(fā),作為一個Java開發(fā),你只需要知道你想在加鎖的時候使用synchronized就可以了,具體的鎖的優(yōu)化是虛擬機根據(jù)競爭情況自行決定的。也就是說,在JDK 1.5 以后,鎖優(yōu)化的這些概念,都被封裝在synchronized中了,對于我們編碼來說是無感知的。
? ? ? ? 提到鎖優(yōu)化,就不得不提一個銀行辦業(yè)務的例子:去銀行辦業(yè)務,你到了銀行之后,要先取一個號,然后你坐在休息區(qū)等待叫號,過段時間,廣播叫到你的號碼之后,會告訴你去哪個柜臺辦理業(yè)務,這時,你拿著你手里的號碼,去到對應的柜臺,找相應的柜員開始辦理業(yè)務。當你辦理業(yè)務的時候,這個柜臺和柜臺后面的柜員只能為你自己服務。當你辦完業(yè)務離開之后,廣播再喊其他的顧客前來辦理業(yè)務。
? ? ? ?在上面這個案例中:每個顧客是一個線程;柜臺前面的那把椅子,就是鎖;柜臺后面的柜員,就是共享資源;無法直接辦理業(yè)務,要取號等待的過程叫做阻塞;當你聽到叫你的號碼的時候,你起身去辦業(yè)務,這就是喚醒;當你坐在椅子上開始辦理業(yè)務的時候,你就獲得鎖;當你辦完業(yè)務離開的時候,你就釋放鎖。
自旋鎖
? ? ? ?synchronized的實現(xiàn)方式中使用Monitor進行加鎖,這是一種互斥鎖,為了表示他對性能的影響我們稱之為重量級鎖。這種互斥鎖在互斥同步上對性能的影響很大,Java的線程是映射到操作系統(tǒng)原生線程之上的,如果要阻塞或喚醒一個線程就需要操作系統(tǒng)的幫忙,這就要從用戶態(tài)轉換到內(nèi)核態(tài),因此狀態(tài)轉換需要花費很多的處理器時間。
? ? ? ?就像去銀行辦業(yè)務的例子,當你來到銀行,發(fā)現(xiàn)柜臺前面都有人的時候,你需要取一個號,然后再去等待區(qū)等待,一直等待被叫號。這個過程是比較浪費時間的,那么有沒有什么辦法改進呢?
? ? ? ?有一種比較好的設計,那就是銀行提供自動取款機,當你去銀行取款的時候,你不需要取號,不需要去休息區(qū)等待叫號,你只需要找到一臺取款機,排在其他人后面等待取款就行了。
? ? ? ?在程序中,Java虛擬機的開發(fā)工程師們在分析過大量數(shù)據(jù)后發(fā)現(xiàn):共享數(shù)據(jù)的鎖定狀態(tài)一般只會持續(xù)很短的一段時間,為了這段時間去掛起和恢復線程其實并不值得。
? ? ? ?如果物理機上有多個處理器,可以讓多個線程同時執(zhí)行的話。我們就可以讓后面來的線程“稍微等一下”,但是并不放棄處理器的執(zhí)行時間,看看持有鎖的線程會不會很快釋放鎖。這個“稍微等一下”的過程就是自旋。即:對于線程而言,無需進入掛起狀態(tài),只要一直保持可運行狀態(tài)即可,這樣一旦前一個線程釋放資源,后一個線程可以立即補上,無需阻塞或喚醒線程。
? ? ? ?自旋鎖和阻塞鎖最大的區(qū)別就是,到底要不要放棄處理器的執(zhí)行時間。對于阻塞鎖和自旋鎖來說,都是要等待獲得共享資源。但是阻塞鎖是放棄了CPU時間,進入了等待區(qū),等待被喚醒。而自旋鎖是一直“自旋”在那里,時刻的檢查共享資源是否可以被訪問。
? ? ? ?由于自旋鎖只是將當前線程不停地執(zhí)行循環(huán)體,不進行線程狀態(tài)的改變,所以響應速度更快。但當線程數(shù)不停增加時,性能下降明顯,因為每個線程都需要執(zhí)行,占用CPU時間。如果線程競爭不激烈,并且保持鎖的時間段。適合使用自旋鎖。
消除鎖
? ? ? ?你去銀行取錢,所有情況下都需要取號,并且等待嗎?其實是不用的,當銀行辦理業(yè)務的人不多的時候,可能根本不需要取號,直接走到柜臺前面辦理業(yè)務就好了。能這么做的前提是,沒有人和你搶著辦業(yè)務。
? ? ? ?這種例子,在鎖優(yōu)化中被稱作“鎖消除”,是JIT編譯器對內(nèi)部鎖的具體實現(xiàn)所做的一種優(yōu)化。在動態(tài)編譯同步塊的時候,JIT編譯器可以借助一種被稱為逃逸分析(Escape Analysis)的技術來判斷同步塊所使用的鎖對象是否只能夠被一個線程訪問而沒有被發(fā)布到其他線程。
public void f() {
Object lock = new Object();
synchronized(lock) {
System.out.println(lock);
}
}
? ? ? ?代碼中對lock這個對象進行加鎖,但是lock對象的生命周期只在f()方法中,并不會被其他線程所訪問到,所以在JIT編譯階段就會被優(yōu)化掉。優(yōu)化成:
public void f() {
Object lock = new Object();
System.out.println(lock);
}
? ? ? ?由于這段優(yōu)化是在JIT階段,不是在前端編譯階段,所以常規(guī)的反編譯方式是無法看到優(yōu)化后的代碼的,但是,如果感興趣,還是可以看的,只是會復雜一點,首先你要自己build一個fasttest版本的jdk,然后在使用java命令對.class文件進行執(zhí)行的時候加上-XX:+PrintEliminateLocks參數(shù)。而且jdk的模式還必須是server模式。
? ? ? ?總之,在使用synchronized的時候,如果JIT經(jīng)過逃逸分析之后發(fā)現(xiàn)并無線程安全問題的話,就會做鎖消除.
鎖粗化
? ? ? ?很多人都知道,在代碼中,需要加鎖的時候,我們提倡盡量減小鎖的粒度,這樣可以避免不必要的阻塞。
? ? ? ?還是我們?nèi)ャy行柜臺辦業(yè)務,最高效的方式是你坐在柜臺前面的時候,只辦和銀行相關的事情。如果這個時候,你拿出手機,接打幾個電話,問朋友要往哪個賬戶里面打錢,這就很浪費時間了。最好的做法肯定是提前準備好相關資料,在辦理業(yè)務時直接辦理就好了。
? ? ? ?加鎖也一樣,把無關的準備工作放到鎖外面,鎖內(nèi)部只處理和并發(fā)相關的內(nèi)容。這樣有助于提高效率。但是當你去銀行辦業(yè)務,你為了減少每次辦理業(yè)務的時間,你把要辦的五個業(yè)務分成五次去辦理,這反而適得其反了。因為這平白的增加了很多你重新取號、排隊、被喚醒的時間。
? ? ? ?如果在一段代碼中連續(xù)的對同一個對象反復加鎖解鎖,其實是相對耗費資源的,這種情況可以適當放寬加鎖的范圍,減少性能消耗。
for(int i=0;i<100000;i++){
synchronized(this){
do();
}
//會被粗化成:
synchronized(this){
for(int i=0;i<100000;i++){
do();
}
? ? ? ?這其實和我們要求的減小鎖粒度并不沖突。減小鎖粒度強調的是不要在銀行柜臺前做準備工作以及和辦理業(yè)務無關的事情。而鎖粗化建議的是,同一個人,要辦理多個業(yè)務的時候,可以在同一個窗口一次性辦完,而不是多次取號多次辦理。
Lock
? ? ? ?synchronized本身是存在一定的缺陷的,如果一個代碼塊被synchronized修飾了,當一個線程獲取了對應的鎖,并執(zhí)行該代碼塊時,其他線程便只能一直等待,等待獲取鎖的線程釋放鎖,而這里獲取鎖的線程釋放鎖只會有兩種情況:
- 獲取鎖的線程執(zhí)行完了該代碼塊,然后線程釋放對鎖的占有
- 線程執(zhí)行發(fā)生異常,此時
JVM會讓線程自動釋放鎖
? ? ? ?這樣導致的一個直接問題就是:如果前一個已經(jīng)獲取到鎖的線程,正在進行IO操作或者sleep等等其他比較耗時的操作,那么在這個線程沒有釋放鎖資源之前,后續(xù)的線程是無法執(zhí)行的,只能無限制等待。但是在實際應用中并發(fā)并不是想象的那么100%發(fā)生,例如:在多個線程對文件進行操作時,讀寫操作屬于沖突操作,寫和寫操作也是沖突操作,但是讀和讀操作是不應該發(fā)生沖突的,此時如果按照synchronized寫法,就會極大地降低程序執(zhí)行的效率。
? ? ? ?Lock的存在就解決了在某些情況下(例如:讀讀操作)線程之間避免發(fā)生沖突。同時Lock有一個特性,可以知道線程又沒有成功獲取到鎖,這個是synchronized所不具備的。
? ? ? ?Lock和synchronized有一點非常大的不同,采用synchronized不需要用戶去手動釋放鎖,當synchronized方法或者synchronized代碼塊執(zhí)行完之后,系統(tǒng)會自動讓線程釋放對鎖的占用;而Lock則必須要用戶去手動釋放鎖,如果沒有主動釋放鎖,就有可能導致出現(xiàn)死鎖現(xiàn)象。
unlock()方法:它是用的最多的方法,用來獲取鎖和釋放鎖,如果已經(jīng)被其他線程獲取,則等待。Lock是需要手動釋放鎖資源的,所以一般它都會用在try-catch語句中,釋放鎖資源的操作一般都放在finally語句塊中。
tryLock()方法:這個方法返回一個布爾值,看方法名就能明確,它是嘗試獲取鎖,如果鎖已經(jīng)被其他線程獲取,此時會返回false,即嘗試獲取鎖失敗,它會立即給出返回值,不會一直等待獲取鎖。
*tryLock(long time, TimeUnit unit):這個同樣是嘗試獲取鎖操作,但是它會等待一定的時間,在等待時間內(nèi)如果獲取到鎖,返回true,否則超過指定的時間,仍然拿不到鎖就返回false。
*lockInterruptibly():這個方法同樣是一個獲取線程鎖的方法,只是比較特殊,如果線程正在等待獲取鎖,在等待期間這個線程是可以被打斷的,即:中斷線程的等待狀態(tài)。假設有A和B兩個線程同時使用lockInterruptibly()方法獲取鎖,此時加入A獲取到了鎖,B就會在等待狀態(tài),此時對線程B可以使用interrupt()方法來終端線程B的等待狀態(tài)。如果線程獲取了鎖之后是不能被中斷的,interrupt方法只能中斷阻塞過程中的線程,而不能中斷正在運行過程中的線程。
-
newCondition():它返回一個Condition實例,調用這個方法有個前提條件,當前線程已經(jīng)獲取到鎖。Condition中存在一個await方法,調用它會以原子的方式釋放鎖,在等待返回之前會重新獲取鎖。
繼承和實現(xiàn)
ReadWriteLock:也是一個接口,只定義了兩個方法readLock和writeLock,返回Lock實例,把文件的讀寫操作分開,分成兩個鎖分配給線程,因此可以有多個線程同時進行讀操作。ReentrantReadWriteLock:如果有一個線程已經(jīng)占用了讀鎖,此時若有其他線程需要申請寫鎖,則申請寫鎖的線程會一直等待釋放讀鎖。如果有一個線程已經(jīng)占用了寫鎖,此時若有其他線程需要申請讀鎖或者寫鎖,申請的線程會一致等待釋放寫鎖。
? ? ? ?Java中synchronized是非公平鎖,它無法保證等待線程獲取鎖的順序,ReentrantLock和ReentrantReadWriteLock默認也是非公平鎖,但是可以進行設置(FairSync和NofairSync)。在new的時候傳入布爾值true或false,true表示公平鎖,反之則是非公平鎖,默認非公平鎖。
對比Lock和synchronized
-
Lock是一個接口,而synchronized是Java內(nèi)置的 -
synchronized在發(fā)生異常時,會自動釋放線程占有的鎖,不會造成死鎖;Lock如果發(fā)生異常,若沒有主動釋放鎖資源,很可能會造成死鎖現(xiàn)象 -
Lock可以讓等待鎖的線程響應中斷,synchronized不行 -
Lock可以得知又沒有獲取到鎖,synchronized不行 -
Lock可以提高多個線程讀操作的效率
重入鎖
public synchronized void m1() {
m2();
}
public synchronized void m2() {
//do sth
}
? ? ? ?這時候如果沒有鎖的可重入特性,就會產(chǎn)生死鎖,因為此時Thread進入m1之后,獲取到了鎖,m2方法同樣需要獲取鎖,如果沒有可重入特性,此時就會等待m1方法內(nèi)釋放鎖,但是m1又必須等待m2執(zhí)行完才能釋放鎖,造成死鎖。
? ? ? ?鎖重入可以類比井口打水的例子:只有一口井,井邊安排一個負責人維護秩序,村民打水,先來的可以先打水,后到的在后面排隊,如果是隊伍首部人的家屬,可以不用排隊,直接打水。這個其實就是公平鎖的原理。而非公平鎖則是:如果后面來的村民看到排頭正在打水,則正常排隊,如果排頭的人剛剛打完水,還未完成交接工作,后來的人可以直接嘗試獲取打水權限,搶到了就直接打水,這就是非公平鎖模型,新來的不一定會乖乖排在隊伍后面,所以會存在有些人會等待很久很久。
? ? ? ?上面的例子中,可以發(fā)現(xiàn)只要你是隊伍首部人的家屬,可以跳過排隊,直接打水,這點類似于上面代碼示例中m1執(zhí)行了之后,在m1方法內(nèi)如果執(zhí)行m2需要獲取鎖,可以直接進入,無需等待鎖釋放的過程。
? ? ? ?可重入其實就是線程有一個state計數(shù)器,當線程獲取鎖之后,state會加1,釋放鎖資源的時候state會減1,因此當已經(jīng)獲取鎖的線程再次申請獲取鎖時,若原來的state為1,此時state就變成了2。只有當state值為0的時候其他線程才能去競爭鎖資源。
