Java之多線程同步

synchronized????關(guān)鍵字

Lock? ? ? ? ? ? ? ? ? 接口

ReentrantLock? ? 類


1. 線程同步問題的引入

測試代碼如下:

RunnableThread
主程序,創(chuàng)建RunnableThread類的對象,同時啟動三個線程

運行結(jié)果:本來是100張票,但是結(jié)果是101次

分析:這是由于程序中當線程名字是Thread-0時,線程休眠10ms,這時線程是阻塞的,且并沒有將ticket減1;這時其他線程正常運行,就會導致線程同步問題


2. java的線程同步關(guān)鍵字synchronized

2.1. 同步方式一

synchronized(非匿名的任意對象){

? ? 線程要操作的共享數(shù)據(jù)

}


修改RunnableThread類,加入線程同步:

加上同步之后,線程就沒有同步異常的問題了

synchronized(obj)中的obj相當于是一個同步鎖,沒有g(shù)et到鎖的線程不能進入同步,在同步中的線程如果沒有運行到synchronized的最后,則不會釋放鎖

另外,加上了線程同步之后,程序的運行速度會明顯減慢


2.2. 同步方式二:同步方法(推薦方式?。?/h2>

好處:代碼簡介,使用清晰,省去synchronized中的 非匿名任意對象,而這個obj對象由本類對象引用this隱式代替,只是沒有寫出來而已。

方式:將線程共享數(shù)據(jù),同步,抽取到一個方法中,在方法的聲明處加入同步關(guān)鍵字

void synchronized shareFunction(){

? ? critical section

}

修改2.1:

說明(了解一下):

? ? 當同步方法是非靜態(tài)方法的時候,obj鎖是它自己this,this被系統(tǒng)隱式處理了

? ? 但當同步方法是靜態(tài)static的時候,obj鎖是本類.class,在這個例子當中就是RunnableTread.class,class是一個屬性。同時,靜態(tài)的同步方法,必須對應(yīng)靜態(tài)的共享變量,這里的ticket必須要用static修飾,因為靜態(tài)方法中是沒有this和supper的


對synchronized的補充:

如果拿到synchronized的線程異常退出了,那么等待鎖的線程是否會一直等待呢?

答案是否定的,當JVM發(fā)現(xiàn)有鎖的線程異常了之后會將它的鎖自動釋放,再由其它等待的線程拿到鎖


引申:

? ? 前面我們介紹過StringBuffer和StringBuilder類

????StringBuffer類說它是一個線程安全的類,現(xiàn)在我們查看StringBuffer的源碼,發(fā)現(xiàn)這個類,除了構(gòu)造方法,其他所有的方法都使用了synchronized關(guān)鍵字修飾

StringBuffer類的部分方法

? ? 而StringBuilder類是一個線程不安全的類,因為源碼中它的方法是沒有synchronized修飾的


3. 從JDK1.5開始的Lock接口替代synchronized關(guān)鍵字

synchronized的缺陷:

如果獲取鎖的線程由于要等待IO或者其它原因(比如sleep)被阻塞了,但是又沒有釋放鎖,其它線程便只能等待,這樣非常影響程序執(zhí)行的效率。因此就需要一種機制:可以不讓線程一直無期限等待下去(比如只等待一定時間或者能夠相應(yīng)中斷),通過Lock就可以辦到。

又假設(shè)當多個線程讀寫文件時,read-write會發(fā)生沖突現(xiàn)象,write-write會發(fā)生沖突,但是read-read不會發(fā)生沖突。如果采用synchronized,就不能讓read-read同時進行,只要有一個線程read,其他想read的線程都只能等待,嚴重影響效率。一次需要一種機制:使得多個線程都只是read時,線程之間不會發(fā)生沖突,通過Lock就可以辦到。

另外通過Lock可以知道線程有沒有成功獲取到鎖,這個是synchronized無法辦到的。


Lock和synchronized的區(qū)別:

? ? Lock是一個接口,不是Java語言內(nèi)置的,synchronized是java語言內(nèi)置的關(guān)鍵字。

? ? Lock與synchronized有一點非常大的不同,采用synchronized不需要用戶區(qū)手動釋放鎖,當synchronized方法或者synchronized代碼塊執(zhí)行完之后,系統(tǒng)會自動讓線程釋放對鎖的占用;而Lock則必須要用戶區(qū)手動釋放鎖,如果沒有主動釋放鎖,就有可能導致出現(xiàn)死鎖。


3.1. Lock接口的方法

void?lock()

????獲取普通鎖,如果鎖已被獲取,則只能等待,功能等同于synchronized關(guān)鍵字。但不同的是Lock后必須unLock鎖,一般來說,Lock必須在try{}catch{}中進行,并且將釋放鎖的操作放在finally塊中進行,以保證鎖一定被釋放,防止死鎖的發(fā)生。

void?lockInterruptibly()

? ? 例:當2個線程同時通過lock.lockInterruptibly()想獲取某個鎖時,假設(shè)A線程獲取到了鎖,那B線程只能等待,那么對B線程調(diào)用threadB.interrupt()方法能夠終端B線程的等待過程。注意,當A線程獲取了鎖后,是不會被interrupt()方法中斷的;因此通過lockInterruptibly()方法獲取鎖時,如果沒有獲取到,只能等待,只有等待狀態(tài)下的線程才是可以響應(yīng)中斷的!

boolean?tryLock()

? ? 嘗試獲取鎖,如果獲取成功返回true,反之返回false。也就是說,這個方法無論如何都不會阻塞等待獲取鎖

boolean?tryLock(long?time,?TimeUnit?unit)

? ? 等待time時間,如果在time時間內(nèi)獲取到鎖返回true,如果阻塞等待time時間內(nèi)沒有獲取到鎖返回false

void?unlock()

? ? 業(yè)務(wù)處理完畢,釋放鎖


3.2. ReentrantLock

3.2.1. lock()-unlock()

使用Lock接口使程序中的應(yīng)用更為靈活,類似于C++中對鎖的使用方式,效果和synchronized關(guān)鍵字是一樣的

下面我用Lock接口的實現(xiàn)類ReentrantLock類來改造售票程序

這里需要將unlock加入到finally中,否則當程序異常的時候,鎖沒有被釋放


3.2.2. tryLock()-unLock()


3.2.3. lockInterruptibly()-unLock()


3.3. ReadWriteLock接口

使用讀寫鎖,可以實現(xiàn)讀寫分離鎖定,讀操作可以并發(fā)進行,寫操作鎖定單個線程

? ? 如果有一個線程已經(jīng)占用了讀鎖,則此時其他線程如果要申請寫鎖,則申請寫鎖的線程會一直等待釋放讀鎖

? ? 如果有一個線程已經(jīng)占用了寫鎖,則此時其他線程如果申請寫鎖或者讀鎖,則申請的新城會一直等待釋放寫鎖

已實現(xiàn)的類:ReentrantReadWriteLock


4. 死鎖問題

下面這個圖是我認為對死鎖問題最形象的描述

用代碼實現(xiàn)上圖死鎖的例子:

創(chuàng)建自定義LockA、LockB類,創(chuàng)建DeadLock類重寫Runnable的run方法,在主程序中起2個線程。

LockA對象
LockB對象

對LockA、LockB類的說明:

? ? 第一次使用private來修飾構(gòu)造函數(shù),作用是為了防止對象被主程序new對象

? ? 使用public static final修飾locka和lockb是為了讓程序能夠靜態(tài)調(diào)用LockA中的locka和LockB中的lockb,并且locka、lockb不能夠被修改

DeadLockRunnable
運行主程序

結(jié)果來看,前4行是同一個線程正常搶到ab鎖或ba鎖并釋放

但是第5,6行一個線程搶到了b鎖,另一個線程搶到了a鎖,這樣就產(chǎn)生了開始圖中的死鎖,2個線程都不會再繼續(xù)運行,都卡在打印處了


5. 線程間通信:線程的等待與喚醒

在多線程運行中,有時候某個線程依賴于其他線程的運行結(jié)果,這樣就需要被依賴的線程去通知依賴線程,那么就會使用線程的等待和喚醒。

所使用的方法:

? ? wait():等待,將正在執(zhí)行的線程釋放其執(zhí)行資格 和 執(zhí)行權(quán),并存儲到線程池中

? ? notify():喚醒,喚醒線程池中被wait()的線程,一次喚醒一個,而且是任意的

? ? notifyAll():喚醒全部線程,將線程池中的所有等待線程喚醒


5.1. 未做等待喚醒示例:

有一個輸入端,有一個輸出端,有一個Person類(類中只有2個成員:姓名和年齡)。要求輸入端只負責輸入Person類的信息,輸出端負責打印Person類的信息

自定義Person類,這里定義成public,沒有g(shù)et、set方法,我們只關(guān)心變量
輸入線程
輸出線程
主程序生成Person類的實例,并開啟輸入輸出線程

查看打印結(jié)果:

打印結(jié)果有異常發(fā)生,kluter和lesslin的年齡有錯誤的現(xiàn)象


5.2. 使用synchronized解決異常問題

修改程序:

Person類和主程序不用修改

InputPerson類和OuputPerson類都在在臨界區(qū)加上synchronized,并且鎖一定要用公共的Person對象

修改的InputPerson類
修改后的OutputPerson類

再運行,發(fā)現(xiàn)沒有異常現(xiàn)象存在了


5.3. 使用等待喚醒來控制輸入輸出

在5.2中,雖然解決了輸出異常的問題,但是若輸入或者輸出線程在某一段時間持續(xù)獲得CPU調(diào)度,那么實際上這個程序沒有太大的意義。

我們希望實現(xiàn)的功能是,當輸入線程輸入Person數(shù)據(jù)時,輸出線程打印這個輸入數(shù)據(jù),那么這樣我們就可以使用等待喚醒的功能。

輸入線程:輸入完成后,等待,等待輸出打印結(jié)束,開始下一次輸入

輸出線程:輸出完成后,等待,等待輸入重新復(fù)制,開始下一次輸出


主程序不變

修改Person類:加一個flag來判斷輸入線程該輸入還是等待,輸出線程該輸出還是等待

修改InputPerson類和OutputPerson類

修改后的Person類
修改后的InputPerson類
修改后的OutputPerson類

輸出結(jié)果:

? ? Kluter 35和Lesslin 27交替出現(xiàn)

引申:

? ? 這里一定要注意wait和notify方法是Object的方法,理論上是可以直接匿名類調(diào)用的(也就是直接寫wait()和notify()),但是這里一定要用Person類的方法,否則會exception:java.lang.IllegalMonitorStateException

如果不寫p.wait(),jvm就不知道你要監(jiān)視的wait和notify的對象是什么!

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

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

  • 進程和線程 進程 所有運行中的任務(wù)通常對應(yīng)一個進程,當一個程序進入內(nèi)存運行時,即變成一個進程.進程是處于運行過程中...
    勝浩_ae28閱讀 5,258評論 0 23
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,740評論 18 399
  • (一)Java部分 1、列舉出JAVA中6個比較常用的包【天威誠信面試題】 【參考答案】 java.lang;ja...
    獨云閱讀 7,265評論 0 62
  • 整理來自互聯(lián)網(wǎng) 1,JDK:Java Development Kit,java的開發(fā)和運行環(huán)境,java的開發(fā)工具...
    Ncompass閱讀 1,618評論 0 6
  • ①?可以少做的事情: 睡覺10h35m,周末綜合癥。 放松3h52m,追劇追綜藝。 ②可以不做的事情:追綜藝追劇。...
    3組30彭唯婧閱讀 260評論 0 0

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