synchronized????關(guān)鍵字
Lock? ? ? ? ? ? ? ? ? 接口
ReentrantLock? ? 類
1. 線程同步問題的引入
測試代碼如下:


運行結(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
}
好處:代碼簡介,使用清晰,省去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)鍵字修飾

? ? 而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類的說明:
? ? 第一次使用private來修飾構(gòu)造函數(shù),作用是為了防止對象被主程序new對象
? ? 使用public static final修飾locka和lockb是為了讓程序能夠靜態(tài)調(diào)用LockA中的locka和LockB中的lockb,并且locka、lockb不能夠被修改


結(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類的信息




查看打印結(jié)果:

5.2. 使用synchronized解決異常問題
修改程序:
Person類和主程序不用修改
InputPerson類和OuputPerson類都在在臨界區(qū)加上synchronized,并且鎖一定要用公共的Person對象


再運行,發(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類



輸出結(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的對象是什么!