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