讓我們來一起聊聊Java中的鎖

----------------------------------------業(yè)精于勤荒于嬉闸昨,形成思?xì)в陔S-------------------------------
  在Java程序中有時(shí)候我們可能需要推遲一些高開銷的對(duì)象初始化操作僵缺,等到使用到這些對(duì)象時(shí)再去初始化皿淋。但要正確實(shí)現(xiàn)線程安全的延時(shí)初始化需要一些技巧型将,否則可能會(huì)出現(xiàn)問題。比如下面使用雙重校驗(yàn)鎖實(shí)現(xiàn)的演示加載的單例模式就是存在線程安全問題的:

/**
 * 使用雙重校驗(yàn)鎖
 */
class Single4 {
    private static Single4 single = null;

    private Single4() {
    }

    public static Single4 getSingleton() {
        if (single == null) {
            synchronized (Single4.class) {
                if (single == null) {
                    single = new Single4();//問題就出現(xiàn)在這里
                }
            }
        }
        return single;
    }
}

問題的根源:single = new Single4();
其實(shí)創(chuàng)建一個(gè)對(duì)象可以分為3步完成的:

1. memory = allocate();//1.為對(duì)象分配內(nèi)存空間
2. ctorInstance(single);//2.初始化對(duì)象
3. single = memory;//3.設(shè)置single指向剛分配的內(nèi)存地址

上面?zhèn)未a中的2和3之間可能會(huì)被重排序润歉,重排序后執(zhí)行的順序是這樣的:

1. memory = allocate();//1.為對(duì)象分配內(nèi)存空間
2. single = memory;//3.設(shè)置single指向剛分配的內(nèi)存地址
                   //這時(shí)候?qū)ο筮€沒有初始化足陨!
3. ctorInstance(single);//2.初始化對(duì)象
時(shí)間 線程A 線程B
t1 A1:分配對(duì)象的內(nèi)存空間
t2 A3:設(shè)置single指向內(nèi)存空間
t3 B1:判斷single是否為空
t4 B2:由于single不為null,線程B將訪問single引用的對(duì)象
t5 A2:初始化對(duì)象
t6 A4:訪問single引用的對(duì)象

由上表也可以看出读虏,當(dāng)線程A中初始化single發(fā)生了重排序责静,并且執(zhí)行完t2后時(shí)間片用完,線程B獲得時(shí)間片盖桥,獲取single時(shí)候灾螃,判斷single不為null,接下來線程B將訪問single引用的對(duì)象揩徊,但此時(shí)對(duì)象還沒有進(jìn)行初始化腰鬼,所以就會(huì)引發(fā)異常!
  聰明的你塑荒,一定想到了:如果不讓創(chuàng)建對(duì)象過程發(fā)生重排序熄赡,不就解決問題啦。是的其實(shí)只要對(duì)上面的代碼做一個(gè)小小的修改就能做到:

/**
 * 使用雙重校驗(yàn)鎖
 */
class Single4 {
    private volatile static Single4 single = null;

    private Single4() {
    }

    public static Single4 getSingleton() {
        if (single == null) {
            synchronized (Single4.class) {
                if (single == null) {
                    single = new Single4();
                }
            }
        }
        return single;
    }

好像沒有什么改變啊齿税,有的只是你沒注意到:
private volatile static Single4 single = null;
  對(duì)本谜,就是在聲明引用的時(shí)候在前面加上volatile關(guān)鍵字,加上volatile后偎窘,在多線程環(huán)境下創(chuàng)建對(duì)象過程中的重排序是被禁止的。它是怎么做到的呢溜在,那么下面就讓我們一起來看看volatile是怎么做到的陌知。

1.volatile

volatile是輕量級(jí)的synchronized,它在多處理器開發(fā)保證了共享變量的“可見性”掖肋∑推希可見性的意思是當(dāng)一個(gè)線程修改了共享變量時(shí),另一個(gè)線程能讀到這個(gè)修改的值志笼。

1.1volatile的實(shí)現(xiàn)原理

Java語言規(guī)范第三版中對(duì)volatile的定義如下:Java編程語言允許線程訪問共享變量沿盅,為了確保共享變量能被準(zhǔn)確和一致地更新,線程應(yīng)該確保通過排他鎖單獨(dú)獲得這個(gè)變量纫溃。
  意思就是說腰涧,如果一個(gè)變量被聲明為volatile,Java內(nèi)存模型會(huì)確保所有線程所有的線程看到這個(gè)變量的值是一致的紊浩。
  volatile是如何保證可見性的呢窖铡?下面我們看看我們?cè)趯?duì)volatile變量進(jìn)行寫操作時(shí)疗锐,cpu會(huì)做什么?
single = new Singleton();//single是volatile變量
轉(zhuǎn)化成匯編代碼如下:
0x01a3deld: movb $0×0,0×1104800(%esi);0x01a3de24: lock add1 $0×0,(%esp);
其中Lock前綴的指令在多核處理器下會(huì)引發(fā)了兩件事:

  1. 將當(dāng)前處理器緩存行的數(shù)據(jù)寫到系統(tǒng)內(nèi)存;
  2. 這個(gè)寫回緩存的操作會(huì)使在其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)失效费彼。

1.2volatile的內(nèi)存語義

volatile寫-讀的內(nèi)存語義

  1. volatile的寫-讀與鎖的釋放-獲取有相同的內(nèi)存效果
  2. volatile寫和鎖的釋放有相同的內(nèi)存語義
  3. volatile讀與鎖的獲取有相同的內(nèi)存語義

其實(shí)理解volatile特性的一個(gè)好方法:
把對(duì)volatile變量的單個(gè)讀/寫滑臊,看成是使用同一個(gè)鎖對(duì)這些單個(gè)讀/寫操作做了同步。
  鎖的happens-before規(guī)則保證釋放鎖和獲取鎖的兩個(gè)線程之間的內(nèi)存可見性箍铲,這意味這對(duì)一個(gè)volatile變量的讀雇卷,總是能看到任意線程對(duì)這個(gè)volatile變量最后的寫入值。
  volatile變量自身有兩個(gè)特性:
原子性:對(duì)于任意單個(gè)volatile變量的讀/寫具有原子性,但是類似與volatileVal++這種復(fù)合操作來說,它就不具有原子性颠猴。
可見性:對(duì)于一個(gè)volatile變量的讀关划,總是能看到任意線程對(duì)這個(gè)volatile變量最后的寫入。

當(dāng)寫一個(gè)volatile變量時(shí)芙粱,JMM會(huì)把線程對(duì)應(yīng)的本地內(nèi)存中的共享變量值刷新到主內(nèi)存


對(duì)volatile變量寫過程

當(dāng)讀一個(gè)volatile變量時(shí)祭玉,JMM會(huì)把線程對(duì)應(yīng)的本地內(nèi)存置為無效,線程接下來將從主內(nèi)存中讀取共享變量春畔。


對(duì)volatile變量讀的過程

注意:
一些編程大牛往往會(huì)告誡我們說脱货,盡量不要去使用volatile,因?yàn)槭褂胿olatile稍有不慎就會(huì)出現(xiàn)問題律姨。如果嚴(yán)格遵循 volatile 的使用條件:

  • 變量真正獨(dú)立于其他變量
  • 不依賴于自己以前的值

如果是多個(gè)volatile操作或類似于volatile++這種復(fù)合操作振峻,這些操作整體上不具有原子性。在某些情況下可以使用volatile代替synchronized來簡(jiǎn)化代碼择份。然而扣孟,使用volatile的代碼往往比使用鎖的代碼更加容易出錯(cuò)。(見volatileDemo)

2.synchronized

在多線程并發(fā)編程中synchronized一直是元老級(jí)角色荣赶,也有很多人稱它為重量級(jí)鎖凤价。但是隨著Java 1.6對(duì)aynchronized進(jìn)行了各種優(yōu)化之后,有些情況下他就并不那么重了拔创。
synchronized的使用場(chǎng)景有一下三種:

  • 對(duì)普通方法的同步利诺,鎖是當(dāng)前實(shí)例的對(duì)象
  • 對(duì)靜態(tài)方法的同步,鎖是當(dāng)前類的Class對(duì)象
  • 對(duì)代碼塊進(jìn)行同步剩燥,鎖是synchronized括號(hào)里配置的對(duì)象

JVM規(guī)范規(guī)定JVM基于進(jìn)入和退出Monitor對(duì)象來實(shí)現(xiàn)方法同步和代碼塊同步慢逾,但兩者的實(shí)現(xiàn)細(xì)節(jié)不一樣。代碼塊同步是使用monitorenter和monitorexit指令實(shí)現(xiàn)灭红,而方法同步是使用另外一種方式實(shí)現(xiàn)的侣滩,細(xì)節(jié)在JVM規(guī)范里并沒有詳細(xì)說明,但是方法的同步同樣可以使用這兩個(gè)指令來實(shí)現(xiàn)变擒。monitorenter指令是在編譯后插入到同步代碼塊的開始位置君珠,而monitorexit是插入到方法結(jié)束處和異常處, JVM要保證每個(gè)monitorenter必須有對(duì)應(yīng)的monitorexit與之配對(duì)娇斑。任何對(duì)象都有一個(gè) monitor 與之關(guān)聯(lián)葛躏,當(dāng)且一個(gè)monitor 被持有后澈段,它將處于鎖定狀態(tài)。線程執(zhí)行到 monitorenter 指令時(shí)舰攒,將會(huì)嘗試獲取對(duì)象所對(duì)應(yīng)的 monitor 的所有權(quán)败富,即嘗試獲得對(duì)象的鎖。

2.1 Java對(duì)象頭

鎖存在Java對(duì)象頭里摩窃。如果對(duì)象是數(shù)組類型兽叮,則虛擬機(jī)用3個(gè)Word(字寬)存儲(chǔ)對(duì)象頭,如果對(duì)象是非數(shù)組類型猾愿,則用2字寬存儲(chǔ)對(duì)象頭鹦聪。在32位虛擬機(jī)中,一字寬等于四字節(jié)蒂秘,即32bit泽本。

java對(duì)象頭

Java對(duì)象頭里的Mark Word里默認(rèn)存儲(chǔ)對(duì)象的HashCode,分代年齡和鎖標(biāo)記位姻僧。32位JVM的Mark Word的默認(rèn)存儲(chǔ)結(jié)構(gòu)如下:

無鎖狀態(tài)的Mark Word

在運(yùn)行期間Mark Word里存儲(chǔ)的數(shù)據(jù)會(huì)隨著鎖標(biāo)志位的變化而變化撇贺。Mark Word可能變化為存儲(chǔ)以下4種數(shù)據(jù):

不同鎖下對(duì)象頭的狀態(tài)

2.2 幾種鎖的類型

線程的阻塞和喚醒需要CPU從用戶態(tài)轉(zhuǎn)為核心態(tài)松嘶,頻繁的阻塞和喚醒對(duì)CPU來說是一件負(fù)擔(dān)很重的工作。

Java SE1.6為了減少獲得鎖和釋放鎖所帶來的性能消耗巢音,引入了“偏向鎖”和“輕量級(jí)鎖”尽超,所以在Java SE1.6里鎖一共有四種狀態(tài),無鎖狀態(tài),偏向鎖狀態(tài)燥狰,輕量級(jí)鎖狀態(tài)和重量級(jí)鎖狀態(tài)棘脐,它會(huì)隨著競(jìng)爭(zhēng)情況逐漸升級(jí)。

鎖可以升級(jí)但不能降級(jí)龙致,意味著偏向鎖升級(jí)成輕量級(jí)鎖后不能降級(jí)成偏向鎖蛀缝。這種鎖升級(jí)卻不能降級(jí)的策略,目的是為了提高獲得鎖和釋放鎖的效率目代。

2.2.1 偏向鎖

Hotspot的作者經(jīng)過以往的研究發(fā)現(xiàn)大多數(shù)情況下鎖不僅不存在多線程競(jìng)爭(zhēng)屈梁,而且總是由同一線程多次獲得嗤练。偏向鎖的目的是在某個(gè)線程獲得鎖之后煞抬,消除這個(gè)線程鎖重入(CAS)的開銷革答,看起來讓這個(gè)線程得到了偏護(hù)曙强。

偏向鎖的進(jìn)一步理解
  偏向鎖的釋放不需要做任何事情碟嘴,這也就意味著加過偏向鎖的MarkValue會(huì)一直保留偏向鎖的狀態(tài)娜扇,因此即便同一個(gè)線程持續(xù)不斷地加鎖解鎖,也是沒有開銷的捎废。
  另一方面登疗,偏向鎖比輕量鎖更容易被終結(jié)辐益,輕量鎖是在有鎖競(jìng)爭(zhēng)出現(xiàn)時(shí)升級(jí)為重量鎖智政,而一般偏向鎖是在有不同線程申請(qǐng)鎖時(shí)升級(jí)為輕量鎖续捂,這也就意味著假如一個(gè)對(duì)象先被線程1加鎖解鎖宦搬,再被線程2加鎖解鎖间校,這過程中沒有鎖沖突憔足,也一樣會(huì)發(fā)生偏向鎖失效,不同的是這回要先退化為無鎖的狀態(tài)控妻,再加輕量鎖饼暑,如圖:

偏向鎖的鎖定和解鎖

另外湿弦,JVM對(duì)那種會(huì)有多線程加鎖勇吊,但不存在鎖競(jìng)爭(zhēng)的情況也做了優(yōu)化疏咐,聽起來比較拗口毕籽,但在現(xiàn)實(shí)應(yīng)用中確實(shí)是可能出現(xiàn)這種情況关筒,因?yàn)榫€程之前除了互斥之外也可能發(fā)生同步關(guān)系蒸播,被同步的兩個(gè)線程(一前一后)對(duì)共享對(duì)象鎖的競(jìng)爭(zhēng)很可能是沒有沖突的。

偏向鎖的獲取
  當(dāng)一個(gè)線程訪問同步塊并獲取鎖時(shí)胀屿,會(huì)在對(duì)象頭和棧幀中的鎖記錄里存儲(chǔ)鎖偏向的線程ID宿崭,以后該線程在進(jìn)入和退出同步塊時(shí)不需要花費(fèi)CAS操作來加鎖和解鎖葡兑,而只需簡(jiǎn)單的測(cè)試一下對(duì)象頭的Mark Word里是否存儲(chǔ)著指向當(dāng)前線程的偏向鎖讹堤,如果測(cè)試成功房资,表示線程已經(jīng)獲得了鎖轰异,如果測(cè)試失敗,則需要再測(cè)試下Mark Word中偏向鎖的標(biāo)識(shí)是否設(shè)置成1(表示當(dāng)前是偏向鎖)婴削,如果沒有設(shè)置唉俗,則使用CAS競(jìng)爭(zhēng)鎖虫溜,如果設(shè)置了衡楞,則嘗試使用CAS將對(duì)象頭的偏向鎖指向當(dāng)前線程瘾境。

偏向鎖的撤銷
  偏向鎖使用了一種等到競(jìng)爭(zhēng)出現(xiàn)才釋放鎖的機(jī)制迷守,所以當(dāng)其他線程嘗試競(jìng)爭(zhēng)偏向鎖時(shí)兑凿,持有偏向鎖的線程才會(huì)釋放鎖急膀。偏向鎖的撤銷卓嫂,需要等待全局安全點(diǎn)(在這個(gè)時(shí)間點(diǎn)上沒有字節(jié)碼正在執(zhí)行)晨雳,它會(huì)首先暫停擁有偏向鎖的線程餐禁,然后檢查持有偏向鎖的線程是否活著帮非,如果線程不處于活動(dòng)狀態(tài)末盔,則將對(duì)象頭設(shè)置成無鎖狀態(tài)陨舱,如果線程仍然活著游盲,擁有偏向鎖的棧會(huì)被執(zhí)行,遍歷偏向?qū)ο蟮逆i記錄谜慌,棧中的鎖記錄和對(duì)象頭的Mark Word要么重新偏向于其他線程畦娄,要么恢復(fù)到無鎖或者標(biāo)記對(duì)象不適合作為偏向鎖熙卡,最后喚醒暫停的線程驳癌。下圖中的線程1演示了偏向鎖初始化的流程颓鲜,線程2演示了偏向鎖撤銷的流程甜滨。

偏向鎖的鎖定和解鎖

偏向鎖的設(shè)置
  關(guān)閉偏向鎖:偏向鎖在Java 6和Java 7里是默認(rèn)啟用的,但是它在應(yīng)用程序啟動(dòng)幾秒鐘之后才激活瘤袖,如有必要可以使用JVM參數(shù)來關(guān)閉延遲-XX:BiasedLockingStartupDelay = 0衣摩。如果你確定自己應(yīng)用程序里所有的鎖通常情況下處于競(jìng)爭(zhēng)狀態(tài),可以通過JVM參數(shù)關(guān)閉偏向鎖-XX:-UseBiasedLocking=false捂敌,那么默認(rèn)會(huì)進(jìn)入輕量級(jí)鎖狀態(tài)艾扮。

2.2.2 輕量級(jí)鎖

輕量級(jí)鎖加鎖
  線程在執(zhí)行同步塊之前,JVM會(huì)先在當(dāng)前線程的棧楨中創(chuàng)建用于存儲(chǔ)鎖記錄的空間占婉,并將對(duì)象頭中的Mark Word復(fù)制到鎖記錄中,官方稱為Displaced Mark Word磺箕。然后線程嘗試使用CAS將對(duì)象頭中的Mark Word替換為指向鎖記錄的指針滞磺。如果成功广凸,當(dāng)前線程獲得鎖,如果失敗扭吁,則自旋獲取鎖,當(dāng)自旋獲取鎖仍然失敗時(shí)枫吧,表示存在其他線程競(jìng)爭(zhēng)鎖(兩條或兩條以上的線程競(jìng)爭(zhēng)同一個(gè)鎖),則輕量級(jí)鎖會(huì)膨脹成重量級(jí)鎖。

輕量級(jí)鎖解鎖
  輕量級(jí)解鎖時(shí)镀层,會(huì)使用原子的CAS操作來將Displaced Mark Word替換回到對(duì)象頭,如果成功惶我,則表示同步過程已完成。如果失敗捧挺,表示有其他線程嘗試過獲取該鎖,則要在釋放鎖的同時(shí)喚醒被掛起的線程黑竞。下圖是兩個(gè)線程同時(shí)爭(zhēng)奪鎖,導(dǎo)致鎖膨脹的流程圖。

輕量級(jí)鎖

2.2.3 重量級(jí)鎖

重量鎖在JVM中又叫對(duì)象監(jiān)視器(Monitor)幅聘,它很像C中的Mutex,除了具備Mutex互斥的功能陵叽,它還負(fù)責(zé)實(shí)現(xiàn)了Semaphore的功能,也就是說它至少包含一個(gè)競(jìng)爭(zhēng)鎖的隊(duì)列,和一個(gè)信號(hào)阻塞隊(duì)列(wait隊(duì)列)独令,前者負(fù)責(zé)做互斥舍败,后一個(gè)用于做線程同步裙戏。

重量級(jí)鎖

2.2.4 鎖的優(yōu)缺點(diǎn)對(duì)比

幾種鎖的比較

2.3 鎖的內(nèi)存語義

鎖是java并發(fā)編程中最重要的同步機(jī)制壹罚。鎖除了讓臨界區(qū)互斥執(zhí)行外,還可以讓釋放鎖的線程向獲取同一個(gè)鎖的線程發(fā)送消息。下面是鎖釋放-獲取的示例代碼:

class MonitorExample {
    int a = 0;

    public synchronized void writer() {  //1
        a++;                             //2
    }                                    //3

    public synchronized void reader() {  //4
        int i = a;                       //5
        ……
    }                                    //6
}

假設(shè)線程A執(zhí)行writer()方法漠吻,隨后線程B執(zhí)行reader()方法。根據(jù)happens before規(guī)則,這個(gè)過程包含的happens before 關(guān)系可以分為兩類:

根據(jù)程序次序規(guī)則试读,1 happens before 2, 2 happens before 3; 4 happens before 5, 5 happens before 6。
根據(jù)監(jiān)視器鎖規(guī)則倘屹,3 happens before 4拍谐。
根據(jù)happens before 的傳遞性力穗,2 happens before 5当窗。

上述happens before 關(guān)系的圖形化表現(xiàn)形式如下:

鎖的內(nèi)存語義

在上圖中,每一個(gè)箭頭鏈接的兩個(gè)節(jié)點(diǎn)庶香,代表了一個(gè)happens before 關(guān)系。黑色箭頭表示程序順序規(guī)則七扰;橙色箭頭表示監(jiān)視器鎖規(guī)則奢赂;藍(lán)色箭頭表示組合這些規(guī)則后提供的happens before保證。

上圖表示在線程A釋放了鎖之后颈走,隨后線程B獲取同一個(gè)鎖膳灶。在上圖中,2 happens before 5立由。因此轧钓,線程A在釋放鎖之前所有可見的共享變量,在線程B獲取同一個(gè)鎖之后锐膜,將立刻變得對(duì)B線程可見聋迎。

鎖釋放和獲取的內(nèi)存語義
  當(dāng)線程釋放鎖時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存中。以上面的MonitorExample程序?yàn)槔珹線程釋放鎖后辉浦,共享數(shù)據(jù)的狀態(tài)示意圖如下:

釋放鎖過程

當(dāng)線程獲取鎖時(shí)甩卓,JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無效缀棍。從而使得被監(jiān)視器保護(hù)的臨界區(qū)代碼必須要從主內(nèi)存中去讀取共享變量。下面是鎖獲取的狀態(tài)示意圖:

鎖的獲取過程

對(duì)比鎖釋放-獲取的內(nèi)存語義與volatile寫-讀的內(nèi)存語義机错,可以看出:鎖釋放與volatile寫有相同的內(nèi)存語義爬范;鎖獲取與volatile讀有相同的內(nèi)存語義。

下面對(duì)鎖釋放和鎖獲取的內(nèi)存語義做個(gè)總結(jié):

線程A釋放一個(gè)鎖弱匪,實(shí)質(zhì)上是線程A向接下來將要獲取這個(gè)鎖的某個(gè)線程發(fā)出了(線程A對(duì)共享變量所做修改的)消息青瀑。
  線程B獲取一個(gè)鎖,實(shí)質(zhì)上是線程B接收了之前某個(gè)線程發(fā)出的(在釋放這個(gè)鎖之前對(duì)共享變量所做修改的)消息萧诫。
  線程A釋放鎖斥难,隨后線程B獲取這個(gè)鎖,這個(gè)過程實(shí)質(zhì)上是線程A通過主內(nèi)存向線程B發(fā)送消息帘饶。

3. java.util.concurrent.locks包下常用的類

下面我們就來探討一下java.util.concurrent.locks包中常用的類和接口哑诊。

3.1 Lock

首先要說明的就是Lock,通過查看Lock的源碼可知及刻,Lock是一個(gè)接口:

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

下面來逐個(gè)講述Lock接口中每個(gè)方法的使用镀裤,lock()竞阐、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用來獲取鎖的暑劝。unLock()方法是用來釋放鎖的骆莹。

在Lock中聲明了四個(gè)方法來獲取鎖,那么這四個(gè)方法有何區(qū)別呢铃岔?
  首先lock()方法是平常使用得最多的一個(gè)方法汪疮,就是用來獲取鎖。如果鎖已被其他線程獲取毁习,則進(jìn)行等待智嚷。
  
  由于在前面講到如果采用Lock,必須主動(dòng)去釋放鎖纺且,并且在發(fā)生異常時(shí)盏道,不會(huì)自動(dòng)釋放鎖。因此一般來說载碌,使用Lock必須在try{}catch{}塊中進(jìn)行猜嘱,并且將釋放鎖的操作放在finally塊中進(jìn)行,以保證鎖一定被被釋放嫁艇,防止死鎖的發(fā)生朗伶。通常使用Lock來進(jìn)行同步的話,是以下面這種形式去使用的:

Lock lock = ...;
lock.lock();
try{
    //處理任務(wù)
}catch(Exception ex){
     
}finally{
    lock.unlock();   //釋放鎖
}

tryLock()方法是有返回值的步咪,它表示用來嘗試獲取鎖论皆,如果獲取成功,則返回true猾漫,如果獲取失數闱纭(即鎖已被其他線程獲取)悯周,則返回false粒督,也就說這個(gè)方法無論如何都會(huì)立即返回。在拿不到鎖時(shí)不會(huì)一直在那等待禽翼。
  
  tryLock(long time,TimeUnitunit)方法和tryLock()方法是類似的屠橄,只不過區(qū)別在于這個(gè)方法在拿不到鎖時(shí)會(huì)等待一定的時(shí)間,在時(shí)間期限之內(nèi)如果還拿不到鎖闰挡,就返回false锐墙。如果如果一開始拿到鎖或者在等待期間內(nèi)拿到了鎖,則返回true解总。

所以贮匕,一般情況下通過tryLock來獲取鎖時(shí)是這樣使用的:

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //處理任務(wù)
     }catch(Exception ex){
         
     }finally{
         lock.unlock();   //釋放鎖
     } 
}else {
    //如果不能獲取鎖,則直接做其他事情
}

lockInterruptibly()方法比較特殊花枫,當(dāng)通過這個(gè)方法去獲取鎖時(shí)刻盐,如果線程正在等待獲取鎖掏膏,則這個(gè)線程能夠響應(yīng)中斷,即中斷線程的等待狀態(tài)敦锌。也就使說馒疹,當(dāng)兩個(gè)線程同時(shí)通過lock.lockInterruptibly()想獲取某個(gè)鎖時(shí),假若此時(shí)線程A獲取到了鎖乙墙,而線程B只有在等待颖变,那么對(duì)線程B調(diào)用threadB.interrupt()方法能夠中斷線程B的等待過程。

由于lockInterruptibly()的聲明中拋出了異常听想,所以lock.lockInterruptibly()必須放在try塊中或者在調(diào)用lockInterruptibly()的方法外聲明拋出InterruptedException腥刹。

因此lockInterruptibly()一般的使用形式如下:

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}

注意,當(dāng)一個(gè)線程獲取了鎖之后汉买,是不會(huì)被interrupt()方法中斷的衔峰。因?yàn)楸旧碓谇懊娴奈恼轮兄v過單獨(dú)調(diào)用interrupt()方法不能中斷正在運(yùn)行過程中的線程,只能中斷阻塞過程中的線程蛙粘。
  因此當(dāng)通過lockInterruptibly()方法獲取某個(gè)鎖時(shí)垫卤,如果不能獲取到,只有進(jìn)行等待的情況下出牧,是可以響應(yīng)中斷的穴肘。
  而用synchronized修飾的話,當(dāng)一個(gè)線程處于等待某個(gè)鎖的狀態(tài)舔痕,是無法被中斷的评抚,只有一直等待下去。

3.2 ReentrantLock

ReentrantLock赵讯,意思是“可重入鎖”盈咳,關(guān)于可重入鎖的概念在下一節(jié)講述耿眉。ReentrantLock是唯一實(shí)現(xiàn)了Lock接口的類边翼,并且ReentrantLock提供了更多的方法。下面通過一些實(shí)例看具體看一下如何使用ReentrantLock鸣剪。

例子1组底,lock()的正確使用方法

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  
     
    public void insert(Thread thread) {
        Lock lock = new ReentrantLock();    //注意這個(gè)地方
        lock.lock();
        try {
            System.out.println(thread.getName()+"得到了鎖");
            for(int i=0;i<5;i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {
            // TODO: handle exception
        }finally {
            System.out.println(thread.getName()+"釋放了鎖");
            lock.unlock();
        }
    }
}

小伙伴們先想一下這段代碼的輸出結(jié)果是什么?

Thread-0得到了鎖
Thread-1得到了鎖
Thread-0釋放了鎖
Thread-1釋放了鎖

也許有朋友會(huì)問筐骇,怎么會(huì)輸出這個(gè)結(jié)果债鸡?第二個(gè)線程怎么會(huì)在第一個(gè)線程釋放鎖之前得到了鎖?原因在于铛纬,在insert方法中的lock變量是局部變量厌均,每個(gè)線程執(zhí)行該方法時(shí)都會(huì)保存一個(gè)副本,那么理所當(dāng)然每個(gè)線程執(zhí)行到lock.lock()處獲取的是不同的鎖告唆,所以就不會(huì)發(fā)生沖突棺弊。

知道了原因改起來就比較容易了晶密,只需要將lock聲明為類的屬性即可。

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();    //注意這個(gè)地方
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  
     
    public void insert(Thread thread) {
        lock.lock();
        try {
            System.out.println(thread.getName()+"得到了鎖");
            for(int i=0;i<5;i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {
            // TODO: handle exception
        }finally {
            System.out.println(thread.getName()+"釋放了鎖");
            lock.unlock();
        }
    }
}

這樣就是正確地使用Lock的方法了模她。

例子2稻艰,tryLock()的使用方法

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();    //注意這個(gè)地方
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  
     
    public void insert(Thread thread) {
        if(lock.tryLock()) {
            try {
                System.out.println(thread.getName()+"得到了鎖");
                for(int i=0;i<5;i++) {
                    arrayList.add(i);
                }
            } catch (Exception e) {
                // TODO: handle exception
            }finally {
                System.out.println(thread.getName()+"釋放了鎖");
                lock.unlock();
            }
        } else {
            System.out.println(thread.getName()+"獲取鎖失敗");
        }
    }
}

輸出結(jié)果:

Thread-0得到了鎖
Thread-1獲取鎖失敗
Thread-0釋放了鎖

例子3,lockInterruptibly()響應(yīng)中斷的使用方法:

public class Test {
    private Lock lock = new ReentrantLock();   
    public static void main(String[] args)  {
        Test test = new Test();
        MyThread thread1 = new MyThread(test);
        MyThread thread2 = new MyThread(test);
        thread1.start();
        thread2.start();
         
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.interrupt();
    }  
     
    public void insert(Thread thread) throws InterruptedException{
        lock.lockInterruptibly();   //注意侈净,如果需要正確中斷等待鎖的線程尊勿,必須將獲取鎖放在外面,然后將InterruptedException拋出
        try {  
            System.out.println(thread.getName()+"得到了鎖");
            long startTime = System.currentTimeMillis();
            for(    ;     ;) {
                if(System.currentTimeMillis() - startTime >= Integer.MAX_VALUE)
                    break;
                //插入數(shù)據(jù)
            }
        }
        finally {
            System.out.println(Thread.currentThread().getName()+"執(zhí)行finally");
            lock.unlock();
            System.out.println(thread.getName()+"釋放了鎖");
        }  
    }
}
 
class MyThread extends Thread {
    private Test test = null;
    public MyThread(Test test) {
        this.test = test;
    }
    @Override
    public void run() {
         
        try {
            test.insert(Thread.currentThread());
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName()+"被中斷");
        }
    }
}

運(yùn)行之后畜侦,發(fā)現(xiàn)thread2能夠被正確中斷元扔。

3.3 ReadWriteLock

ReadWriteLock也是一個(gè)接口,在它里面只定義了兩個(gè)方法:

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading.
     */
    Lock readLock();
 
    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing.
     */
    Lock writeLock();
}

一個(gè)用來獲取讀鎖旋膳,一個(gè)用來獲取寫鎖摇展。也就是說將文件的讀寫操作分開,分成2個(gè)鎖來分配給線程溺忧,從而使得多個(gè)線程可以同時(shí)進(jìn)行讀操作咏连。下面的ReentrantReadWriteLock實(shí)現(xiàn)了ReadWriteLock接口。

3.4 ReentrantReadWriteLock

ReentrantReadWriteLock里面提供了很多豐富的方法鲁森,不過最主要的有兩個(gè)方法:readLock()和writeLock()用來獲取讀鎖和寫鎖祟滴。

下面通過幾個(gè)例子來看一下ReentrantReadWriteLock具體用法。

假如有多個(gè)線程要同時(shí)進(jìn)行讀操作的話歌溉,先看一下synchronized達(dá)到的效果:

public class Test {
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
     
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
    }  
     
    public synchronized void get(Thread thread) {
        long start = System.currentTimeMillis();
        while(System.currentTimeMillis() - start <= 1) {
            System.out.println(thread.getName()+"正在進(jìn)行讀操作");
        }
        System.out.println(thread.getName()+"讀操作完畢");
    }
}

這段程序的輸出結(jié)果會(huì)是垄懂,直到thread1執(zhí)行完讀操作之后,才會(huì)打印thread2執(zhí)行讀操作的信息痛垛。

Thread-0正在進(jìn)行讀操作
Thread-0正在進(jìn)行讀操作
Thread-0正在進(jìn)行讀操作
Thread-0正在進(jìn)行讀操作
Thread-0正在進(jìn)行讀操作
Thread-0正在進(jìn)行讀操作
Thread-0讀操作完畢
Thread-1正在進(jìn)行讀操作
Thread-1正在進(jìn)行讀操作
Thread-1讀操作完畢

而改成用讀寫鎖的話:

public class Test {
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
     
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
    }  
     
    public void get(Thread thread) {
        rwl.readLock().lock();
        try {
            long start = System.currentTimeMillis();
             
            while(System.currentTimeMillis() - start <= 1) {
                System.out.println(thread.getName()+"正在進(jìn)行讀操作");
            }
            System.out.println(thread.getName()+"讀操作完畢");
        } finally {
            rwl.readLock().unlock();
        }
    }
}

此時(shí)打印的結(jié)果為:

Thread-0正在進(jìn)行讀操作
Thread-0正在進(jìn)行讀操作
Thread-1正在進(jìn)行讀操作
Thread-1正在進(jìn)行讀操作
Thread-1正在進(jìn)行讀操作
Thread-0正在進(jìn)行讀操作
Thread-0正在進(jìn)行讀操作
Thread-0讀操作完畢
Thread-1讀操作完畢

說明thread1和thread2在同時(shí)進(jìn)行讀操作草慧。

這樣就大大提升了讀操作的效率

不過要注意的是,如果有一個(gè)線程已經(jīng)占用了讀鎖匙头,則此時(shí)其他線程如果要申請(qǐng)寫鎖漫谷,則申請(qǐng)寫鎖的線程會(huì)一直等待釋放讀鎖。

如果有一個(gè)線程已經(jīng)占用了寫鎖蹂析,則此時(shí)其他線程如果申請(qǐng)寫鎖或者讀鎖舔示,則申請(qǐng)的線程會(huì)一直等待釋放寫鎖。

關(guān)于ReentrantReadWriteLock類中的其他方法感興趣的朋友可以自行查閱API文檔电抚。

3.5 Lock和synchronized的選擇

總結(jié)來說惕稻,Lock和synchronized有以下幾點(diǎn)不同:
  1)Lock是一個(gè)接口,而synchronized是Java中的關(guān)鍵字蝙叛,synchronized是內(nèi)置的語言實(shí)現(xiàn)俺祠;
  2)synchronized在發(fā)生異常時(shí),會(huì)自動(dòng)釋放線程占有的鎖,因此不會(huì)導(dǎo)致死鎖現(xiàn)象發(fā)生蜘渣;而Lock在發(fā)生異常時(shí)妓布,如果沒有主動(dòng)通過unLock()去釋放鎖,則很可能造成死鎖現(xiàn)象宋梧,因此使用Lock時(shí)需要在finally塊中釋放鎖匣沼;
  3)Lock可以讓等待鎖的線程響應(yīng)中斷,而synchronized卻不行捂龄,使用synchronized時(shí)释涛,等待的線程會(huì)一直等待下去,不能夠響應(yīng)中斷倦沧;
  4)通過Lock可以知道有沒有成功獲取鎖唇撬,而synchronized卻無法辦到。
  5)Lock可以提高多個(gè)線程進(jìn)行讀操作的效率展融。

在性能上來說窖认,如果競(jìng)爭(zhēng)資源不激烈,兩者的性能是差不多的告希,而當(dāng)競(jìng)爭(zhēng)資源非常激烈時(shí)(即有大量線程同時(shí)競(jìng)爭(zhēng))扑浸,此時(shí)Lock的性能要遠(yuǎn)遠(yuǎn)優(yōu)于synchronized。所以說燕偶,在具體使用時(shí)要根據(jù)適當(dāng)情況選擇喝噪。

參考文獻(xiàn)
《Java并發(fā)編程藝術(shù)》
http://ifeve.com/java-memory-model-5/
http://blog.csdn.net/true100/article/details/51693715 luojinping.com/2015/07/09/java%E9%94%81%E4%BC%98%E5%8C%96/
http://blog.jobbole.com/104902/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市指么,隨后出現(xiàn)的幾起案子酝惧,更是在濱河造成了極大的恐慌,老刑警劉巖伯诬,帶你破解...
    沈念sama閱讀 221,273評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件晚唇,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡盗似,警方通過查閱死者的電腦和手機(jī)哩陕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來桥言,“玉大人萌踱,你說我怎么就攤上這事葵礼『虐ⅲ” “怎么了?”我有些...
    開封第一講書人閱讀 167,709評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵鸳粉,是天一觀的道長(zhǎng)扔涧。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么枯夜? 我笑而不...
    開封第一講書人閱讀 59,520評(píng)論 1 296
  • 正文 為了忘掉前任弯汰,我火速辦了婚禮,結(jié)果婚禮上湖雹,老公的妹妹穿的比我還像新娘咏闪。我一直安慰自己,他們只是感情好摔吏,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,515評(píng)論 6 397
  • 文/花漫 我一把揭開白布鸽嫂。 她就那樣靜靜地躺著,像睡著了一般征讲。 火紅的嫁衣襯著肌膚如雪据某。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,158評(píng)論 1 308
  • 那天诗箍,我揣著相機(jī)與錄音癣籽,去河邊找鬼。 笑死滤祖,一個(gè)胖子當(dāng)著我的面吹牛筷狼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播匠童,決...
    沈念sama閱讀 40,755評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼桑逝,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了俏让?” 一聲冷哼從身側(cè)響起楞遏,我...
    開封第一講書人閱讀 39,660評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎首昔,沒想到半個(gè)月后寡喝,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,203評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡勒奇,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,287評(píng)論 3 340
  • 正文 我和宋清朗相戀三年预鬓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赊颠。...
    茶點(diǎn)故事閱讀 40,427評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡格二,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出竣蹦,到底是詐尸還是另有隱情顶猜,我是刑警寧澤,帶...
    沈念sama閱讀 36,122評(píng)論 5 349
  • 正文 年R本政府宣布痘括,位于F島的核電站长窄,受9級(jí)特大地震影響滔吠,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜挠日,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,801評(píng)論 3 333
  • 文/蒙蒙 一疮绷、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧嚣潜,春花似錦冬骚、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至犯犁,卻和暖如春属愤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背酸役。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工住诸, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人涣澡。 一個(gè)月前我還...
    沈念sama閱讀 48,808評(píng)論 3 376
  • 正文 我出身青樓贱呐,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親入桂。 傳聞我的和親對(duì)象是個(gè)殘疾皇子奄薇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,440評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • Java8張圖 11、字符串不變性 12抗愁、equals()方法馁蒂、hashCode()方法的區(qū)別 13、...
    Miley_MOJIE閱讀 3,707評(píng)論 0 11
  • 從三月份找實(shí)習(xí)到現(xiàn)在蜘腌,面了一些公司沫屡,掛了不少,但最終還是拿到小米撮珠、百度沮脖、阿里、京東芯急、新浪勺届、CVTE、樂視家的研發(fā)崗...
    時(shí)芥藍(lán)閱讀 42,272評(píng)論 11 349
  • 參考鏈接:http://smallbug-vip.iteye.com/blog/2275743 在多線程開發(fā)的過程...
    時(shí)之令閱讀 1,557評(píng)論 2 5
  • 《紅塵別戀》目錄 上一章 面試模特 -28- 阿霞病了 羅默加班了娶耍。他從下午五點(diǎn)半一直加班到晚上八點(diǎn)免姿,總算忙完手頭...
    林嘉梓閱讀 575評(píng)論 9 37
  • 生活中的你或許卑微,或許輝煌伺绽,但到底哪個(gè)才是真正的你养泡,只有你自己明白嗜湃!以前的我總覺得自己就這樣挺好奈应,當(dāng)個(gè)平凡的人挺...
    檸汐熙閱讀 470評(píng)論 0 0