Java多線程 -- 04 線程同步

導(dǎo)讀目錄
  • 同步代碼塊
  • 同步方法
  • 釋放同步監(jiān)視器的鎖定(仔細看)
  • 同步鎖(Lock)
  • Lock和synchronized的選擇
  • 鎖的相關(guān)概念介紹
  • 死鎖

多線程情況下出現(xiàn)的錯誤往往是因為線程調(diào)度(該調(diào)度具有一定的隨機性)引起的,不過這種錯誤是可以從程序編寫上來避免的

1.同步代碼塊

為了解決代碼這些問題,Java多線程支持引入了同步監(jiān)視器,使用同步監(jiān)視器的通用方法就是同步代碼塊,格式如下:

//該段代碼塊往往被放置在方法體內(nèi)初婆,且是在run()或call()方法體中
synchronized(obj) {
    //此處的代碼就是同步代碼塊
    ...
}

其中的obj是這段同步代碼塊的同步監(jiān)視器抓督,這段代碼的含義是:線程開始執(zhí)行同步代碼塊之前,必須先獲得同步監(jiān)視器的鎖定

這段代碼執(zhí)行的過程:加鎖 -> 修改 -> 釋放所

注:
1.雖然Java程序允許使用任何對象作為同步監(jiān)視器璃哟,但推薦使用可能被并發(fā)訪問的共享資源充當(dāng)同步監(jiān)視器归园。例如銀行取錢例子中亭枷,選擇將使用賬戶(account)作為同步監(jiān)視器。
2.共享資源的代碼區(qū)也被稱為臨界區(qū)掏熬,如上面的同步代碼塊
3.synchronized關(guān)鍵字可以修飾代碼塊和方法佑稠,但不能修飾構(gòu)造器和成員變量

2.同步方法

用synchronized來修飾某個方法,則該方法就被稱為同步方法旗芬,synchronized修飾的實例方法(即非靜態(tài)方法)而言舌胶,其監(jiān)視器無需顯示指定,是this,即方法調(diào)用者

使用同步方法可以很方便的實現(xiàn)線程安全的類疮丛,(加了同步方法的類變成了線程安全的類 )該類具有的特點:
1.該類的對象可以被多個線程安全的訪問
2.每個線程調(diào)用該對象的任意的方法之后都能得到正確的結(jié)果
3.每個線程調(diào)用該對象的任意方法之后幔嫂,該對象狀態(tài)依然保持合理狀態(tài)

//銀行取錢的例子:賬戶類
public class Account {
    ...
    //同步方法: 取錢的操作, 
    public synchronized void draw(double drawAmount) {
        ...
    }
}

注意:
1.不要對線程安全類的所有方法都同步(為了盡量保證程序的效率),只對那些共享資源加同步
2.如果可變類有兩種運行環(huán)境:單線程誊薄、多線程環(huán)境履恩,則應(yīng)該為該可變類提供兩個版本,即線程不安全版本和線程安全版本暇屋。在單線程中使用線程不安全的版本似袁,以保證性能。在多線程環(huán)境下使用多線程版本咐刨,以保證安全
3.不可變類總是線程安全的昙衅,而可變類往往是線程不安全的。將可變類設(shè)置成線程安全的是以犧牲其運行效率為代價的

3.釋放同步監(jiān)視器的鎖定

任何線程進入同步代碼塊定鸟、同步方法之前鸳碧,都必須先獲得對同步監(jiān)視器的鎖定,處理完資源后熏迹,又得釋放對同步監(jiān)視器的鎖定,而程序是無法顯式釋放這個鎖定材原,那么線程在什么情況在會釋放對同步監(jiān)視器的鎖定?
1.當(dāng)前線程的同步方法季眷、同步代碼塊正常執(zhí)行結(jié)束余蟹;
2.當(dāng)前線程在同步代碼塊、同步方法中遇到break子刮,return終止了該代碼塊威酒、方法的繼續(xù)執(zhí)行
3.當(dāng)前線程在同步代碼塊、同步方法中出現(xiàn)了未處理的Error, Exception, 導(dǎo)致了該代碼塊挺峡、方法異常結(jié)束
4.當(dāng)前線程在執(zhí)行同步代碼塊葵孤、同步方法時,程序執(zhí)行了同步監(jiān)視器對象的wait()方法橱赠,則當(dāng)前線程暫停尤仍,并釋放同步監(jiān)視器

請注意:下面的情況并不會導(dǎo)致線程釋放同步監(jiān)視器:
1.當(dāng)前線程在執(zhí)行同步代碼塊、同步方法時狭姨,程序調(diào)用了Thread.sleep(), Thread.yield()方法來暫停當(dāng)前線程的執(zhí)行宰啦,當(dāng)前線程不是釋放同步監(jiān)視器
2.線程執(zhí)行同步代碼塊時,其他線程調(diào)用了該線程的suspend()方法(即在一個線程中讓其他的線程執(zhí)行suspend()方法)將該線程掛起送挑,則該線程是不會釋放同步監(jiān)視器绑莺。當(dāng)然了,要盡量避免使用suspend()方法和resume()方法來控制線程

4.同步鎖(Lock)

這種情況下是不存在同步監(jiān)視器的惕耕,該Lock對象被稱為同步鎖纺裁。
前面講的同步代碼塊和同步方法中,當(dāng)一個線程獲取了對應(yīng)的鎖司澎,并執(zhí)行該代碼塊時欺缘,其他線程便只能一直等待,直到等待到線程釋放鎖挤安,那么如果這個得到鎖的線程由于要等待IO或者其他原因(比如調(diào)用sleep方法)被阻塞了谚殊,但是又沒有釋放鎖,其他線程便只能干巴巴地等待蛤铜。

因此就需要有一種機制可以不讓等待的線程一直無期限地等待下去(比如只等待一定的時間或者能夠響應(yīng)中斷)

再比如當(dāng)有多個線程讀寫文件時嫩絮,讀操作和寫操作會發(fā)生沖突,寫操作和寫操作會發(fā)生沖突围肥,但是讀操作和讀操作不會發(fā)生沖突現(xiàn)象剿干。

Java5以后,提供了一種功能更強大的線程同步機制:通過顯式定義同步鎖對象來實現(xiàn)同步穆刻,該所對象由Lock對象來充當(dāng)置尔,就可以滿足上面說的要求

也就是說Lock提供了比synchronized更多的功能。但是要注意以下幾點:
(1)Lock不是Java語言內(nèi)置的氢伟,synchronized是Java語言的關(guān)鍵字榜轿,因此是內(nèi)置特性幽歼。Lock是一個類,通過這個類可以實現(xiàn)同步訪問谬盐;
(2)Lock和synchronized有一點非常大的不同甸私,采用synchronized不需要用戶去手動釋放鎖,當(dāng)synchronized方法或者synchronized代碼塊執(zhí)行完之后设褐,系統(tǒng)會自動讓線程釋放對鎖的占用颠蕴;而Lock則必須要用戶去手動釋放鎖,如果沒有主動釋放鎖助析,就有可能導(dǎo)致出現(xiàn)死鎖現(xiàn)象, 注意:必須主動去釋放鎖,并且在發(fā)生異常時椅您,不會自動釋放鎖

(1)Lock鎖
Lock<>(根接口)
    |
    ReentrantLook外冀,可重入鎖, 常用

//Lock的源碼

public interface Lock {
    void lock(); //用來獲取鎖。如果鎖已被其他線程獲取掀泳,則進行等待
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock(); //嘗試獲取鎖雪隧,如果獲取成功,則返回true,反之返回false, 即不會等待著獲取鎖
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //嘗試獲取鎖,拿不到鎖時會等待 time 時間, 在此時間內(nèi)直到獲取到(并返回true)员舵,或者沒有等待到(并返回false)
    void unlock(); //釋放鎖
    Condition newCondition(); //用于線程通信中
}

方法講解:
**(1) void lock();**
用來獲取鎖脑沿。如果鎖已被其他線程獲取,則進行等待
銀行取錢的例子, 使用lock()方法時:

public class Account {
//定義鎖對象
private final ReentrantLock reLock = new ReentrantLock();
....
//取錢操作
public void draw(double drawAccount) {
//加鎖
reLock.lock();
try{

        ... //取錢的邏輯代碼
    }finally {

        //釋放鎖,放在這里是為了確保鎖一定能被釋放马僻,及時在發(fā)生異常情況后
        reLock.unlock();
    }       
}

}


**(2) boolean tryLock();**
嘗試獲取鎖庄拇,如果獲取成功,則返回true, 反之返回false韭邓。該方法會立即返回結(jié)果措近,不會因為沒有得到鎖而等待著獲取鎖

**(3) boolean tryLock(long time, TimeUnit unit) throws InterruptedException;** 
嘗試獲取鎖,拿不到鎖時會等待time時間, 在此時間內(nèi)直到獲取到(并返回true),或者沒有等待到(并返回false)
采用tryLock()方法時:

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


**void lockInterruptibly() throws InterruptedException;**
lockInterruptibly()方法比較特殊瞭郑,當(dāng)通過這個方法去獲取鎖時,如果線程正在等待獲取鎖鸭你,則這個線程能夠響應(yīng)中斷(即調(diào)用該線程的interrupt()方法)屈张,即停止等待。其他的方法和synchronized修飾的代碼塊和方法都不會相應(yīng)該中斷

//注意這里要處理InterruptedException異常
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}


**注意:**當(dāng)一個線程獲取了鎖之后袱巨,是不會被interrupt()方法中斷的阁谆。因為單獨調(diào)用interrupt()方法(Thread類的方法)不能中斷**正在運行**過程中的線程,只能中斷**阻塞過程**中的線程瓣窄。



######(2)ReadWriteLock
讀寫鎖笛厦,允許對共享資源并發(fā)訪問

ReadWriteLock<>(根接口)
|
ReentrantReadWriteLook(可重入讀寫鎖)
StampedLock (Java8新增的)

**(1)采用ReentrantReadWriteLock鎖**
在對數(shù)據(jù)進行讀寫的時候,為了保證數(shù)據(jù)的一致性和完整性俺夕,需要**讀和寫是互斥**的裳凸,**寫和寫是互斥**的贱鄙,但是**讀和讀是不需要互斥**的,這樣讀和讀不互斥性能更高些

ReentrantReadWriteLock里面提供了很多豐富的方法姨谷,不過最主要的有兩個方法:**readLock()**和**writeLock()**用來獲取讀鎖和寫鎖

public class Data {
//定義可讀寫鎖
private ReadWriteLock rwl = new ReentrantReadWriteLock();
...
//寫數(shù)據(jù)
public void set(int data) {
rwl.writeLock().lock();// 取到寫鎖
try {
... //處理過程
} finally {
rwl.writeLock().unlock();// 釋放寫鎖
}
}
//讀數(shù)據(jù)
public void get() {
rwl.readLock().lock();// 取到讀鎖
try {
... //處理過程
} finally {
rwl.readLock().unlock();// 釋放讀鎖
}
}
}


**注意:**的是逗宁,如果有一個線程已經(jīng)占用了讀鎖,則此時其他線程如果要申請寫鎖梦湘,則申請寫鎖的線程會一直等待釋放讀鎖瞎颗。如果有一個線程已經(jīng)占用了寫鎖,則此時其他線程如果申請寫鎖或者讀鎖捌议,則申請的線程會一直等待釋放寫鎖哼拔。


######5.Lock和synchronized的選擇
總結(jié)來說,Lock和synchronized有以下幾點不同:
1)Lock是一個接口瓣颅,而synchronized是Java中的關(guān)鍵字倦逐,synchronized是內(nèi)置的語言實現(xiàn);
2)synchronized在發(fā)生異常時宫补,會自動釋放線程占有的鎖檬姥,因此不會導(dǎo)致死鎖現(xiàn)象發(fā)生;而Lock在發(fā)生異常時粉怕,如果沒有主動通過unLock()去釋放鎖健民,則很可能造成死鎖現(xiàn)象,因此使用Lock時需要在finally塊中釋放鎖贫贝;
3)Lock可以讓等待鎖的線程響應(yīng)中斷(需要使用lockInterruptibly()方法)秉犹,而synchronized卻不行,使用synchronized時平酿,等待的線程會一直等待下去凤优,不能夠響應(yīng)中斷;
4)通過Lock可以知道有沒有成功獲取鎖蜈彼,而synchronized卻無法辦到筑辨。
5)Lock可以提高多個線程進行讀操作的效率。**記仔夷妗:寫/寫互拆棍辕,讀/寫互拆,讀/讀不互拆**

在性能上來說还绘,如果競爭資源不激烈楚昭,兩者的性能是差不多的,而當(dāng)競爭資源非常激烈時(即有大量線程同時競爭)拍顷,此時Lock的性能要遠遠優(yōu)于synchronized抚太。所以說,在具體使用時要根據(jù)適當(dāng)情況選擇。


**注意:**上面介紹的三種同步方法尿贫,都遵循了一個規(guī)則: ***加鎖 -> 修改 -> 釋放鎖 ***


######6.鎖的相關(guān)概念介紹
**1.可重入鎖**
如果鎖具備可重入性(即不需要重復(fù)申請鎖)电媳,則稱作為可重入鎖。像**synchronized**和**ReentrantLock庆亡、ReentrantReadWriteLock**都是可重入鎖匾乓。可重入性在我看來實際上表明了鎖的分配機制:基于線程的分配又谋,而不是基于方法調(diào)用的分配拼缝。

class MyClass {
public synchronized void method1() {
method2();
}

public synchronized void method2() {
    ...
}

}

上述代碼中的兩個方法method1和method2都用synchronized修飾了,假如某一時刻彰亥,線程A執(zhí)行到了method1咧七,此時線程A獲取了這個對象的鎖,而由于method2也是synchronized方法任斋,假如synchronized不具備可重入性猪叙,此時線程A需要重新申請鎖。但是這就會造成一個問題仁卷,因為線程A已經(jīng)持有了該對象的鎖,而又在申請獲取該對象的鎖犬第,這樣就會線程A一直等待永遠不會獲取到的鎖锦积。而由于synchronized和Lock都具備可重入性,所以不會發(fā)生上述現(xiàn)象歉嗓。


**2.可中斷鎖**
可中斷鎖:就是可以相應(yīng)中斷的鎖丰介。在Java中,synchronized就不是可中斷鎖鉴分,而Lock是可中斷鎖, 即lockInterruptibly()方法哮幢。


**3.公平鎖**
**公平鎖**即盡量以請求鎖的順序來獲取鎖。比如同是有多個線程在等待一個鎖志珍,當(dāng)這個鎖被釋放時橙垢,等待時間最久的線程(最先請求的線程)會獲得該所,這種就是公平鎖伦糯。

**非公平鎖**即無法保證鎖的獲取是按照請求鎖的順序進行的柜某。這樣就可能導(dǎo)致某個或者一些線程永遠獲取不到鎖。

在Java中敛纲,synchronized就是非公平鎖喂击,它無法保證等待的線程獲取鎖的順序。而對于ReentrantLock和ReentrantReadWriteLock淤翔,它默認(rèn)情況下是非公平鎖(無參構(gòu)造器)翰绊,但是可以設(shè)置為公平鎖(用有參構(gòu)造器)。
``
ReentrantLock lock = new ReentrantLock(); //為非公平鎖
ReentrantLock lock = new ReentrantLock(true); //true為公平鎖,false為非公平鎖

4.讀寫鎖
讀寫鎖將對一個資源(比如文件)的訪問分成了2個鎖监嗜,一個讀鎖和一個寫鎖谐檀。正因為有了讀寫鎖,才使得多個線程之間的讀操作不會發(fā)生沖突秤茅。ReadWriteLock就是讀寫鎖稚补,它是一個接口,ReentrantReadWriteLock實現(xiàn)了這個接口框喳】文唬可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖五垮。

7.死鎖

當(dāng)兩個線程相互等待對方釋放鎖同步監(jiān)視器時就會放生死鎖現(xiàn)象乍惊,Java虛擬機并沒有檢測(而在數(shù)據(jù)庫系統(tǒng)的設(shè)計中考慮了監(jiān)測死鎖以及從死鎖中恢復(fù),數(shù)據(jù)庫如果監(jiān)測到了一組事物發(fā)生了死鎖時放仗,將選擇一個犧牲者并放棄這個事物)润绎,也沒有采取措施來處理死鎖,所以所線程編程時應(yīng)該采取措施避免死鎖出現(xiàn)

出現(xiàn)死鎖诞挨,整個程序不會發(fā)生任何異常莉撇,也不會給出任何提示,只是所有線程一直處于阻塞狀態(tài)惶傻,無法繼續(xù)執(zhí)行

死鎖很容易發(fā)生棍郎,尤其是當(dāng)系統(tǒng)中有多個同步監(jiān)視器時

(1)產(chǎn)生死鎖的案例及原因

1.最簡單的死鎖案例:
Java中死鎖最簡單的情況是,一個線程T1持有鎖L1并且申請獲得鎖L2银室,而另一個線程T2持有鎖L2并且申請獲得鎖L1涂佃,因為默認(rèn)的鎖申請操作都是阻塞的,所以線程T1和T2永遠被阻塞了蜈敢。導(dǎo)致了死鎖

2.稍微復(fù)雜點的案例
多個線程形成了一個死鎖的環(huán)路辜荠,比如:線程T1持有鎖L1并且申請獲得鎖L2,而線程T2持有鎖L2并且申請獲得鎖L3抓狭,而線程T3持有鎖L3并且申請獲得鎖L1伯病,這樣導(dǎo)致了一個鎖依賴的環(huán)路:T1依賴T2的鎖L2,T2依賴T3的鎖L3辐宾,而T3依賴T1的鎖L1狱从。從而導(dǎo)致了死鎖。

產(chǎn)生死鎖可能性的最根本原因是:
(1)鎖交叉現(xiàn)象:線程在獲得一個鎖L1的情況下再去申請另外一個鎖L2叠纹,也就是鎖L1在沒有釋放鎖L1的情況下季研,又去申請獲得鎖L2,這個是產(chǎn)生死鎖的最根本原因誉察。
(2)阻塞:另一個原因是默認(rèn)的鎖申請操作是阻塞的与涡。

(2)如何避免產(chǎn)生死鎖

1.避免鎖交叉:避免在一個對象的同步方法中調(diào)用其它對象的同步方法(會造成鎖交叉),那么就可以避免死鎖產(chǎn)生的可能性
2.使用非阻塞式的鎖:使用非阻塞式的鎖,例如Lock的tryLock()鎖,獲取不到鎖時驼卖,會釋放自己已獲得鎖氨肌,并睡眠一小段時間,過會再重新申請酌畜。這樣就會打破鎖交叉現(xiàn)象
3.縮小鎖范圍:盡量避免使用靜態(tài)同步方法怎囚,因為靜態(tài)同步相當(dāng)于全局鎖, 而我們要盡量減小鎖范圍
4.盡量避免然一個線程執(zhí)行過程中同時只需要一把鎖(這個方法不太現(xiàn)實,但是一種方法桥胞,看看就好)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末恳守,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子贩虾,更是在濱河造成了極大的恐慌催烘,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件缎罢,死亡現(xiàn)場離奇詭異伊群,居然都是意外死亡,警方通過查閱死者的電腦和手機策精,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門舰始,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人咽袜,你說我怎么就攤上這事蔽午。” “怎么了酬蹋?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長抽莱。 經(jīng)常有香客問我范抓,道長,這世上最難降的妖魔是什么食铐? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任匕垫,我火速辦了婚禮,結(jié)果婚禮上虐呻,老公的妹妹穿的比我還像新娘象泵。我一直安慰自己,他們只是感情好斟叼,可當(dāng)我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布偶惠。 她就那樣靜靜地躺著,像睡著了一般朗涩。 火紅的嫁衣襯著肌膚如雪忽孽。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天,我揣著相機與錄音兄一,去河邊找鬼厘线。 笑死,一個胖子當(dāng)著我的面吹牛出革,可吹牛的內(nèi)容都是我干的造壮。 我是一名探鬼主播,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼骂束,長吁一口氣:“原來是場噩夢啊……” “哼耳璧!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起栖雾,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤楞抡,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后析藕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體召廷,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年账胧,在試婚紗的時候發(fā)現(xiàn)自己被綠了竞慢。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡治泥,死狀恐怖筹煮,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情居夹,我是刑警寧澤败潦,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站准脂,受9級特大地震影響劫扒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜狸膏,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一沟饥、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧湾戳,春花似錦贤旷、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至韧衣,卻和暖如春县遣,著一層夾襖步出監(jiān)牢的瞬間糜颠,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工萧求, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留其兴,地道東北人。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓夸政,卻偏偏與公主長得像元旬,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子守问,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,700評論 2 354

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