主要談?wù)勬i的性能以及其它一些理論知識(shí),內(nèi)容主要的出處是《Java Concurrency in Practice》,結(jié)合自己的理解和實(shí)際應(yīng)用對(duì)鎖機(jī)制進(jìn)行一個(gè)小小的總結(jié)。
首先需要強(qiáng)調(diào)的一點(diǎn)是:所有鎖(包括內(nèi)置鎖和高級(jí)鎖)都是有性能消耗的,也就是說在高并發(fā)的情況下,由于鎖機(jī)制帶來的上下文切換、資源同步等消耗是非常可觀的。在某些極端情況下,線程在鎖上的消耗可能比線程本身的消耗還要多。所以如果可能的話,在任何情況下都盡量少用鎖,如果不可避免那么采用非阻塞算法是一個(gè)不錯(cuò)的解決方案,但是卻也不是絕對(duì)的。
內(nèi)部鎖
Java語(yǔ)言通過synchronized關(guān)鍵字來保證原子性。這是因?yàn)槊恳粋€(gè)Object都有一個(gè)隱含的鎖,這個(gè)也稱作監(jiān)視器對(duì)象。在進(jìn)入synchronized之前自動(dòng)獲取此內(nèi)部鎖,而一旦離開此方式(不管通過和中方式離開此方法)都會(huì)自動(dòng)釋放鎖。顯然這是一個(gè)獨(dú)占鎖,每個(gè)鎖請(qǐng)求之間是互斥的。相對(duì)于前面介紹的眾多高級(jí)鎖(Lock/ReadWriteLock等),synchronized的代價(jià)都比后者要高。但是synchronized的語(yǔ)法比較簡(jiǎn)單,而且也比較容易使用和理解,不容易寫法上的錯(cuò)誤。而我們知道Lock一旦調(diào)用了lock()方法獲取到鎖而未正確釋放的話很有可能就死鎖了。所以Lock的釋放操作總是跟在finally代碼塊里面,這在代碼結(jié)構(gòu)上也是一次調(diào)整和冗余。另外前面介紹中說過Lock的實(shí)現(xiàn)已經(jīng)將硬件資源用到了極致,所以未來可優(yōu)化的空間不大,除非硬件有了更高的性能。但是synchronized只是規(guī)范的一種實(shí)現(xiàn),這在不同的平臺(tái)不同的硬件還有很高的提升空間,未來Java在鎖上的優(yōu)化也會(huì)主要在這上面。
性能
由于鎖總是帶了性能影響,所以是否使用鎖和使用鎖的場(chǎng)合就變得尤為重要。如果在一個(gè)高并發(fā)的Web請(qǐng)求中使用了強(qiáng)制的獨(dú)占鎖,那么就可以發(fā)現(xiàn)Web的吞吐量將急劇下降。
為了利用并發(fā)來提高性能,出發(fā)點(diǎn)就是:更有效的利用現(xiàn)有的資源,同時(shí)讓程序盡可能的開拓更多可用的資源。這意味著機(jī)器盡可能的處于忙碌的狀態(tài),通常意義是說CPU忙于計(jì)算,而不是等待。當(dāng)然CPU要做有用的事情,而不是進(jìn)行無謂的循環(huán)。當(dāng)然在實(shí)踐中通常會(huì)預(yù)留一些資源出來以便應(yīng)急特殊情況,這在以后的線程池并發(fā)中可以看到很多例子。
線程阻塞
鎖機(jī)制的實(shí)現(xiàn)通常需要操作系統(tǒng)提供支持,顯然這會(huì)增加開銷。當(dāng)鎖競(jìng)爭(zhēng)的時(shí)候,失敗的線程必然會(huì)發(fā)生阻塞。JVM既能自旋等待(不斷嘗試,知道成功,很多CAS就是這樣實(shí)現(xiàn)的),也能夠在操作系統(tǒng)中掛起阻塞的線程,直到超時(shí)或者被喚醒。通常情況下這取決于上下文切換的開銷以及與獲取鎖需要等待的時(shí)間二者之間的關(guān)系。自旋等待適合于比較短的等待,而掛起線程比較適合那些比較耗時(shí)的等待。
掛起一個(gè)線程可能是因?yàn)闊o法獲取到鎖,或者需要某個(gè)特定的條件,或者耗時(shí)的I/O操作。掛起一個(gè)線程需要兩次額外的上下文切換以及操作系統(tǒng)、緩存等多資源的配合:如果線程被提前換出,那么一旦拿到鎖或者條件滿足,那么又需要將線程換回執(zhí)行隊(duì)列,這對(duì)線程而言,兩次上下文切換可能比較耗時(shí)。
鎖競(jìng)爭(zhēng)
影響鎖競(jìng)爭(zhēng)性的條件有兩個(gè):鎖被請(qǐng)求的頻率和每次持有鎖的時(shí)間。顯然當(dāng)而這二者都很小的時(shí)候,鎖競(jìng)爭(zhēng)不會(huì)成為主要的瓶頸。但是如果鎖使用不當(dāng),導(dǎo)致二者都比較大,那么很有可能CPU不能有效的處理任務(wù),任務(wù)被大量堆積。
所以減少鎖競(jìng)爭(zhēng)的方式有下面三種:
- 減少鎖持有的時(shí)間
- 減少鎖請(qǐng)求的頻率
- 采用共享鎖取代獨(dú)占鎖
死鎖
如果一個(gè)線程永遠(yuǎn)不釋放另外一個(gè)線程需要的資源那么就會(huì)導(dǎo)致死鎖。這有兩種情況:一種情況是線程A永遠(yuǎn)不釋放鎖,結(jié)果B一直拿不到鎖,所以線程B就“死掉”了;第二種情況下,線程A擁有線程B需要的鎖Y,同時(shí)線程B擁有線程A需要的鎖X,那么這時(shí)候線程A/B互相依賴對(duì)方釋放鎖,于是二者都“死掉”了。
還有一種情況為發(fā)生死鎖,如果一個(gè)線程總是不能被調(diào)度,那么等待此線程結(jié)果的線程可能就死鎖了。這種情況叫做線程饑餓死鎖。比如說在前面介紹的非公平鎖中,如果某些線程非常活躍,在高并發(fā)情況下這類線程可能總是拿到鎖,那么那些活躍度低的線程可能就一直拿不到鎖,這樣就發(fā)生了“饑餓死”。
避免死鎖的解決方案是:盡可能的按照鎖的使用規(guī)范請(qǐng)求鎖,另外鎖的請(qǐng)求粒度要小(不要在不需要鎖的地方占用鎖,鎖不用了盡快釋放);在高級(jí)鎖里面總是使用tryLock或者定時(shí)機(jī)制(這個(gè)以后會(huì)講,就是指定獲取鎖超時(shí)的時(shí)間,如果時(shí)間到了還沒有獲取到鎖那么就放棄)。高級(jí)鎖(Lock)里面的這兩種方式可以有效的避免死鎖。
活鎖
活鎖描述的是線程總是嘗試某項(xiàng)操作卻總是失敗的情況。這種情況下盡管線程沒有被阻塞,但是人物卻總是不能被執(zhí)行。比如在一個(gè)死循環(huán)里面總是嘗試做某件事,結(jié)果卻總是失敗,現(xiàn)在線程將永遠(yuǎn)不能跳出這個(gè)循環(huán)。另外一種情況是在一個(gè)隊(duì)列中每次從隊(duì)列頭取出一個(gè)任務(wù)來執(zhí)行,每次都失敗,然后將任務(wù)放入隊(duì)列頭,接下來再一次從隊(duì)列頭取出任務(wù)執(zhí)行,仍然失敗。
還有一種活鎖方式發(fā)生在“碰撞協(xié)讓”情況下:兩個(gè)人過獨(dú)木橋,如果在半路相撞,雙方禮貌退出去然后再試一次。如果總是失敗,那么這兩個(gè)任務(wù)將一直無法得到執(zhí)行。
總之解決鎖問題的關(guān)鍵就是:從簡(jiǎn)單的開始,先保證正確,然后再開始優(yōu)化。
©2009-2014 IMXYLZ
|求賢若渴