一、概述
鎖的使用與實(shí)現(xiàn)
Lock接口(顯式地獲取鎖和釋放鎖)
擁有了鎖獲取與釋放的可操作性、可中斷的獲取鎖以及超時(shí)獲取鎖等多種synchronized關(guān)鍵字所不具備的同步特性。不要將獲取鎖的過(guò)程寫(xiě)在try塊中,因?yàn)槿绻讷@取鎖(自定義鎖的實(shí)現(xiàn))時(shí)發(fā)生了異常,異常拋出的同時(shí),也會(huì)導(dǎo)致鎖無(wú)故釋放。 Lock接口的實(shí)現(xiàn)基本都是通過(guò)聚合了一個(gè)同步器的子類(lèi)來(lái)完成線程訪問(wèn)控制的。
隊(duì)列同步器
鎖和同步器的關(guān)系:鎖是面向使用者的,它定義了使用者與鎖交互的接口(比如可以允許兩個(gè)線程并行訪問(wèn)),隱藏了實(shí)現(xiàn)細(xì)節(jié);同步器面向的是鎖的實(shí)現(xiàn)者,它簡(jiǎn)化了鎖的實(shí)現(xiàn)方式,屏蔽了同步狀態(tài)管理、線程的排隊(duì)、等待與喚醒等底層操作。鎖和同步器很好地隔離了使用者和實(shí)現(xiàn)者所需關(guān)注的領(lǐng)域。
從實(shí)現(xiàn)角度分析同步器是如何完成線程同步:
同步隊(duì)列:
同步隊(duì)列中的節(jié)點(diǎn)(Node)用來(lái)保存獲取同步狀態(tài)失敗的線程引用、等待狀態(tài)以及前驅(qū)和后繼節(jié)點(diǎn)。節(jié)點(diǎn)是構(gòu)成同步隊(duì)列(等待隊(duì)列)的基礎(chǔ),同步器擁有首節(jié)點(diǎn)(head)和尾節(jié)點(diǎn)(tail),沒(méi)有成功獲取同步狀態(tài)的線程將會(huì)成為節(jié)點(diǎn)加入該隊(duì)列的尾部。加入隊(duì)列的過(guò)程必須要保證線程安全,因此同步器提供了一個(gè)基于CAS的設(shè)置尾節(jié)點(diǎn)的方法:compareAndSetTail(Node expect,Node update),它需要傳遞當(dāng)前線程“認(rèn)為”的尾節(jié)點(diǎn)和當(dāng)前節(jié)點(diǎn),只有設(shè)置成功后,當(dāng)前節(jié)點(diǎn)才正式與之前的尾節(jié)點(diǎn)建立關(guān)聯(lián)。設(shè)置首節(jié)點(diǎn)是通過(guò)獲取同步狀態(tài)成功的線程來(lái)完成的,由于只有一個(gè)線程能夠成功獲取到同步狀態(tài),因此設(shè)置頭節(jié)點(diǎn)的方法并不需要使用CAS來(lái)保證,它只需要將首節(jié)點(diǎn)設(shè)置成為原首節(jié)點(diǎn)的后繼節(jié)點(diǎn)并斷開(kāi)原首節(jié)點(diǎn)的next引用即可。
獨(dú)占式同步狀態(tài)獲取與釋放:
通過(guò)調(diào)用同步器的acquire(int arg)方法可以獲取同步狀態(tài),該方法對(duì)中斷不敏感,也就是由于線程獲取同步狀態(tài)失敗后進(jìn)入同步隊(duì)列中,后續(xù)對(duì)線程進(jìn)行中斷操作時(shí),線程不會(huì)從同步隊(duì)列中移出。
為什么用CAS設(shè)置尾節(jié)點(diǎn)的方法(compareAndSetTail)?
答:如果使用一個(gè)普通的LinkedList來(lái)維護(hù)節(jié)點(diǎn)之間的關(guān)系,那么當(dāng)一個(gè)線程獲取了同步狀態(tài),而其他多個(gè)線程由于調(diào)用tryAcquire(int arg)方法獲取同步狀態(tài)失敗而并發(fā)地被添加到LinkedList時(shí),LinkedList將難以保證Node的正確添加,最終的結(jié)果可能是節(jié)點(diǎn)的數(shù)量有偏差,而且順序也是混亂的。
在enq(final Node node)方法中,同步器通過(guò)“死循環(huán)”來(lái)保證節(jié)點(diǎn)的正確添加,在“死循環(huán)”中只有通過(guò)CAS將節(jié)點(diǎn)設(shè)置成為尾節(jié)點(diǎn)之后,當(dāng)前線程才能從該方法返回,否則,當(dāng)前線程不斷地嘗試設(shè)置??梢钥闯?,enq(final Node node)方法將并發(fā)添加節(jié)點(diǎn)的請(qǐng)求通過(guò)CAS變得“串行化”了。在acquireQueued(final Node node,int arg)方法中,當(dāng)前線程在“死循環(huán)”中嘗試獲取同步狀態(tài),而只有前驅(qū)節(jié)點(diǎn)是頭節(jié)點(diǎn)才能夠嘗試獲取同步狀態(tài),這是為什么?原因有兩個(gè)。
第一,頭節(jié)點(diǎn)是成功獲取到同步狀態(tài)的節(jié)點(diǎn),而頭節(jié)點(diǎn)的線程釋放了同步狀態(tài)之后,將會(huì)喚醒其后繼節(jié)點(diǎn),后繼節(jié)點(diǎn)的線程被喚醒后需要檢查自己的前驅(qū)節(jié)點(diǎn)是否是頭節(jié)點(diǎn)。
第二,維護(hù)同步隊(duì)列的FIFO原則。
總結(jié):在獲取同步狀態(tài)時(shí),同步器維護(hù)一個(gè)同步隊(duì)列,獲取狀態(tài)失敗的線程都會(huì)被加入到隊(duì)列中并在隊(duì)列中進(jìn)行自旋;移出隊(duì)列(或停止自旋)的條件是前驅(qū)節(jié)點(diǎn)為頭節(jié)點(diǎn)且成功獲取了同步狀態(tài)。在釋放同步狀態(tài)時(shí),同步器調(diào)用tryRelease(int arg)方法釋放同步狀態(tài),然后喚醒頭節(jié)點(diǎn)的后繼節(jié)點(diǎn)。
共享式同步狀態(tài)獲取與釋放:
共享式獲取與獨(dú)占式獲取最主要的區(qū)別在于同一時(shí)刻能否有多個(gè)線程同時(shí)獲取到同步狀態(tài)。以文件的讀寫(xiě)為例,如果一個(gè)程序在對(duì)文件進(jìn)行讀操作,那么這一時(shí)刻對(duì)于該文件的寫(xiě)操作均被阻塞,而讀操作能夠同時(shí)進(jìn)行。寫(xiě)操作要求對(duì)資源的獨(dú)占式訪問(wèn),而讀操作可以是共享式訪問(wèn),兩種不同的訪問(wèn)模式在同一時(shí)刻對(duì)文件或資源的訪問(wèn)情況。對(duì)于能夠支持多個(gè)線程同時(shí)訪問(wèn)的并發(fā)組件(比如Semaphore),它和獨(dú)占式主要區(qū)別在于tryReleaseShared(int arg)方法必須確保同步狀態(tài)(或者資源數(shù))線程安全釋放,一般是通過(guò)循環(huán)和CAS來(lái)保證的,因?yàn)獒尫磐綘顟B(tài)的操作會(huì)同時(shí)來(lái)自多個(gè)線程。
獨(dú)占式超時(shí)獲取同步狀態(tài)
通過(guò)調(diào)用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以超時(shí)獲取同步狀態(tài),即在指定的時(shí)間段內(nèi)獲取同步狀態(tài),如果獲取到同步狀態(tài)則返回true,否則,返回false。該方法提供了傳統(tǒng)Java同步操作(synchronized)所不具備的特性。
重入鎖(ReentrantLock)
它表示該鎖能夠支持一個(gè)線程對(duì)資源的重復(fù)加鎖。除此之外,該鎖還支持獲取鎖時(shí)的公平和非公平性選擇。synchronized關(guān)鍵字隱式的支持重進(jìn)入,比如一個(gè)synchronized修飾的遞歸方法,在方法執(zhí)行時(shí),執(zhí)行線程在獲取了鎖之后仍能連續(xù)多次地獲得該鎖,而不像Mutex由于獲取了鎖,而在下一次獲取鎖時(shí)出現(xiàn)阻塞自己的情況。
①實(shí)現(xiàn)重進(jìn)入
重進(jìn)入是指任意線程在獲取到鎖之后能夠再次獲取該鎖而不會(huì)被鎖所阻塞,該特性的實(shí)現(xiàn)需要解決以下兩個(gè)問(wèn)題。
1)線程再次獲取鎖
2)鎖的最終釋放
②公平性獲取鎖
ReentrantLock是通過(guò)組合自定義同步器來(lái)實(shí)現(xiàn)鎖的獲取與釋放(公平非公平)。公平性與否是針對(duì)獲取鎖而言的,如果一個(gè)鎖是公平的,那么鎖的獲取順序就應(yīng)該符合請(qǐng)求的絕對(duì)時(shí)間順序,也就是FIFO。對(duì)于非公平鎖,只要CAS設(shè)置同步狀態(tài)成功,則表示當(dāng)前線程獲取了鎖,而公平鎖則不同。與nonfairTryAcquire(int acquires)比較,唯一不同的位置為判斷條件多了hasQueuedPredecessors()方法,即加入了同步隊(duì)列中當(dāng)前節(jié)點(diǎn)是否有前驅(qū)節(jié)點(diǎn)的判斷,如果該方法返回true,則表示有線程比當(dāng)前線程更早地請(qǐng)求獲取鎖,因此需要等待前驅(qū)線程獲取并釋放鎖之后才能繼續(xù)獲取鎖。
公平性鎖每次都是從同步隊(duì)列中的第一個(gè)節(jié)點(diǎn)獲取到鎖,而非公平性鎖出現(xiàn)了一個(gè)線程連續(xù)獲取鎖的情況。公平性鎖保證了鎖的獲取按照FIFO原則,而代價(jià)是進(jìn)行大量的線程切換。非公平性鎖雖然可能造成線程“饑餓”,但極少的線程切換,保證了其更大的吞吐量。
讀寫(xiě)鎖(ReentrantReadWriteLock)
允許多個(gè)線程并發(fā)的訪問(wèn)共享資源。Mutex和ReentrantLock基本都是排他鎖,這些鎖在同一時(shí)刻只允許一個(gè)線程進(jìn)行訪問(wèn),而讀寫(xiě)鎖在同一時(shí)刻可以允許多個(gè)讀線程訪問(wèn),但是在寫(xiě)線程訪問(wèn)時(shí),所有的讀線程和其他寫(xiě)線程均被阻塞。讀寫(xiě)鎖維護(hù)了一對(duì)鎖,一個(gè)讀鎖和一個(gè)寫(xiě)鎖,通過(guò)分離讀鎖和寫(xiě)鎖,使得并發(fā)性相比一般的排他鎖有了很大提升。
讀寫(xiě)鎖的實(shí)現(xiàn)分析:
①讀寫(xiě)狀態(tài)的設(shè)計(jì)
讀寫(xiě)鎖同樣依賴(lài)自定義同步器來(lái)實(shí)現(xiàn)同步功能,而讀寫(xiě)狀態(tài)就是其同步器的同步狀態(tài)。
②寫(xiě)鎖的獲取與釋放
寫(xiě)鎖是一個(gè)支持重進(jìn)入的排它鎖。
③讀鎖的獲取與釋放
讀鎖是一個(gè)支持重進(jìn)入的共享鎖。
④鎖降級(jí)
鎖降級(jí)指的是寫(xiě)鎖降級(jí)成為讀鎖。鎖降級(jí)是指把持?。ó?dāng)前擁有的)寫(xiě)鎖,再獲取到讀鎖,隨后釋放(先前擁有的)寫(xiě)鎖的過(guò)程。鎖降級(jí)中讀鎖的獲取是否必要呢?答案是必要的。主要是為了保證數(shù)據(jù)的可見(jiàn)性,如果當(dāng)前線程不獲取讀鎖而是直接釋放寫(xiě)鎖,假設(shè)此刻另一個(gè)線程(記作線程T)獲取了寫(xiě)鎖并修改了數(shù)據(jù),那么當(dāng)前線程無(wú)法感知線程T的數(shù)據(jù)更新。如果當(dāng)前線程獲取讀鎖,即遵循鎖降級(jí)的步驟,則線程T將會(huì)被阻塞,直到當(dāng)前線程使用數(shù)據(jù)并釋放讀鎖之后,線程T才能獲取寫(xiě)鎖進(jìn)行數(shù)據(jù)更新。
RentrantReadWriteLock不支持鎖升級(jí)(把持讀鎖、獲取寫(xiě)鎖,最后釋放讀鎖的過(guò)程)。目的也是保證數(shù)據(jù)可見(jiàn)性,如果讀鎖已被多個(gè)線程獲取,其中任意線程成功獲取了寫(xiě)鎖并更新了數(shù)據(jù),則其更新對(duì)其他獲取到讀鎖的線程是不可見(jiàn)的。
LockSupport工具
有阻塞對(duì)象的parkNanos方法能夠傳遞給開(kāi)發(fā)人員更多的信息。這是由于在Java 5之前,當(dāng)線程阻塞(使用synchronized關(guān)鍵字)在一個(gè)對(duì)象上時(shí),通過(guò)線程dump能夠查看到該線程的阻塞對(duì)象,方便問(wèn)題定位,而Java 5推出的Lock等并發(fā)工具時(shí)卻遺漏了這一點(diǎn),致使在線程dump時(shí)無(wú)法提供阻塞對(duì)象的信息。因此,在Java 6中,LockSupport新增了上述3個(gè)含有阻塞對(duì)象的park方法,用以替代原有的park方法。
Condition接口
監(jiān)視器方法object的四個(gè)方法(wait()、wait(long timeout),notify(),notifyAll()),這些方法與synchronized同步關(guān)鍵字配合,可以實(shí)現(xiàn)等待/通知模式。Condition也提供監(jiān)視器方法,與Lock配合可以實(shí)現(xiàn)等待/通知模式。Condition的使用方式比較簡(jiǎn)單,需要注意在調(diào)用方法前獲取鎖。lock.lock();有界隊(duì)列(任務(wù)jobs隊(duì)列):空取滿填都會(huì)進(jìn)入阻塞狀態(tài)。在添加和刪除方法中使用while循環(huán)而非if判斷,目的是防止過(guò)早或意外的通知,只有條件符合才能夠退出循環(huán)。Condition實(shí)現(xiàn):ConditionObject是同步器AbstractQueuedSynchronizer的內(nèi)部類(lèi),因?yàn)镃ondition的操作需要獲取相關(guān)聯(lián)的鎖,所以作為同步器的內(nèi)部類(lèi)也較為合理。每個(gè)Condition對(duì)象都包含著一個(gè)隊(duì)列(以下稱(chēng)為等待隊(duì)列),該隊(duì)列是Condition對(duì)象實(shí)現(xiàn)等待/通知功能的關(guān)鍵。