Java多線程 -- 04 線程同步

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

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

1.同步代碼塊

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

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

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

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

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

2.同步方法

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

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

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

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

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

任何線程進(jìn)入同步代碼塊、同步方法之前,都必須先獲得對(duì)同步監(jiān)視器的鎖定,處理完資源后,又得釋放對(duì)同步監(jiān)視器的鎖定,而程序是無法顯式釋放這個(gè)鎖定,那么線程在什么情況在會(huì)釋放對(duì)同步監(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í)行同步代碼塊、同步方法時(shí),程序執(zhí)行了同步監(jiān)視器對(duì)象的wait()方法,則當(dāng)前線程暫停,并釋放同步監(jiān)視器

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

4.同步鎖(Lock)

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

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

再比如當(dāng)有多個(gè)線程讀寫文件時(shí),讀操作和寫操作會(huì)發(fā)生沖突,寫操作和寫操作會(huì)發(fā)生沖突,但是讀操作和讀操作不會(huì)發(fā)生沖突現(xiàn)象。

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

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

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

//Lock的源碼

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

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

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

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

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

}


**(2) boolean tryLock();**
嘗試獲取鎖,如果獲取成功,則返回true, 反之返回false。該方法會(huì)立即返回結(jié)果,不會(huì)因?yàn)闆]有得到鎖而等待著獲取鎖

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

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


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

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


**注意:**當(dāng)一個(gè)線程獲取了鎖之后,是不會(huì)被interrupt()方法中斷的。因?yàn)閱为?dú)調(diào)用interrupt()方法(Thread類的方法)不能中斷**正在運(yùn)行**過程中的線程,只能中斷**阻塞過程**中的線程。



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

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

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

ReentrantReadWriteLock里面提供了很多豐富的方法,不過最主要的有兩個(gè)方法:**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();// 釋放讀鎖
}
}
}


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


######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)中斷(需要使用lockInterruptibly()方法),而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)情況選擇。


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


######6.鎖的相關(guān)概念介紹
**1.可重入鎖**
如果鎖具備可重入性(即不需要重復(fù)申請(qǐng)鎖),則稱作為可重入鎖。像**synchronized**和**ReentrantLock、ReentrantReadWriteLock**都是可重入鎖??芍厝胄栽谖铱磥韺?shí)際上表明了鎖的分配機(jī)制:基于線程的分配,而不是基于方法調(diào)用的分配。

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

public synchronized void method2() {
    ...
}

}

上述代碼中的兩個(gè)方法method1和method2都用synchronized修飾了,假如某一時(shí)刻,線程A執(zhí)行到了method1,此時(shí)線程A獲取了這個(gè)對(duì)象的鎖,而由于method2也是synchronized方法,假如synchronized不具備可重入性,此時(shí)線程A需要重新申請(qǐng)鎖。但是這就會(huì)造成一個(gè)問題,因?yàn)榫€程A已經(jīng)持有了該對(duì)象的鎖,而又在申請(qǐng)獲取該對(duì)象的鎖,這樣就會(huì)線程A一直等待永遠(yuǎn)不會(huì)獲取到的鎖。而由于synchronized和Lock都具備可重入性,所以不會(huì)發(fā)生上述現(xiàn)象。


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


**3.公平鎖**
**公平鎖**即盡量以請(qǐng)求鎖的順序來獲取鎖。比如同是有多個(gè)線程在等待一個(gè)鎖,當(dāng)這個(gè)鎖被釋放時(shí),等待時(shí)間最久的線程(最先請(qǐng)求的線程)會(huì)獲得該所,這種就是公平鎖。

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

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

4.讀寫鎖
讀寫鎖將對(duì)一個(gè)資源(比如文件)的訪問分成了2個(gè)鎖,一個(gè)讀鎖和一個(gè)寫鎖。正因?yàn)橛辛俗x寫鎖,才使得多個(gè)線程之間的讀操作不會(huì)發(fā)生沖突。ReadWriteLock就是讀寫鎖,它是一個(gè)接口,ReentrantReadWriteLock實(shí)現(xiàn)了這個(gè)接口??梢酝ㄟ^readLock()獲取讀鎖,通過writeLock()獲取寫鎖。

7.死鎖

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

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

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

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

1.最簡(jiǎn)單的死鎖案例:
Java中死鎖最簡(jiǎn)單的情況是,一個(gè)線程T1持有鎖L1并且申請(qǐng)獲得鎖L2,而另一個(gè)線程T2持有鎖L2并且申請(qǐng)獲得鎖L1,因?yàn)槟J(rèn)的鎖申請(qǐng)操作都是阻塞的,所以線程T1和T2永遠(yuǎn)被阻塞了。導(dǎo)致了死鎖

2.稍微復(fù)雜點(diǎn)的案例
多個(gè)線程形成了一個(gè)死鎖的環(huán)路,比如:線程T1持有鎖L1并且申請(qǐng)獲得鎖L2,而線程T2持有鎖L2并且申請(qǐng)獲得鎖L3,而線程T3持有鎖L3并且申請(qǐng)獲得鎖L1,這樣導(dǎo)致了一個(gè)鎖依賴的環(huán)路:T1依賴T2的鎖L2,T2依賴T3的鎖L3,而T3依賴T1的鎖L1。從而導(dǎo)致了死鎖。

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

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容