Java中的鎖主要分為悲觀鎖與樂觀鎖、公平鎖與非公平鎖、可重入鎖與不可重入鎖、獨占鎖與共享鎖等類型。1.悲觀鎖如synchronized和reentrantlock適用于寫多場景,每次操作都加鎖保證數據一致性;2.樂觀鎖通過版本號或cas實現,適用于讀多寫少的場景,提高吞吐量;3.公平鎖按申請順序獲取鎖避免饑餓現象,但性能較低,而非公平鎖效率高但可能導致線程饑餓;4.可重入鎖允許同一線程多次獲取同一把鎖,避免死鎖,如synchronized和reentrantlock;5.獨占鎖一次只能被一個線程持有,而共享鎖允許多個線程同時訪問,如readwritelock的讀鎖;6.自旋鎖通過循環等待鎖釋放,適用于鎖持有時間短的場景;7.jvm還對鎖進行了優化,引入了鎖升級機制,包括偏向鎖、輕量級鎖和重量級鎖,根據競爭激烈程度進行狀態轉換,以提升性能。
Java中的鎖種類繁多,理解它們能幫助我們寫出更高效、更可靠的并發程序。簡單來說,可以從不同的維度進行分類,比如悲觀鎖/樂觀鎖、公平鎖/非公平鎖、可重入鎖/不可重入鎖、獨占鎖/共享鎖等等。掌握這些鎖的特性,才能在實際場景中選擇最合適的鎖,避免死鎖和性能瓶頸。
悲觀鎖與樂觀鎖的本質區別和應用場景
悲觀鎖,顧名思義,總是假設最壞的情況,每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。Java中synchronized和ReentrantLock等獨占鎖就是悲觀鎖思想的實現。悲觀鎖適用于寫操作非常多的場景,先加鎖可以保證寫操作時數據正確。
立即學習“Java免費學習筆記(深入)”;
樂觀鎖則相對樂觀,它假設數據一般情況下不會造成沖突,所以在數據進行提交更新的時候,才會正式對數據的沖突與否進行檢測,如果發現沖突了,則返回用戶錯誤的信息,讓用戶決定如何去做。樂觀鎖的實現方式通常是使用版本號機制或者CAS算法。在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式:CAS實現的。樂觀鎖適用于讀多寫的場景,這樣可以提高程序的吞吐量。
公平鎖與非公平鎖的區別與選擇
公平鎖是指多個線程按照申請鎖的順序來獲取鎖,類似排隊,先到先得。而非公平鎖則允許線程“插隊”,即后來的線程可能比先來的線程更快獲取到鎖。
Java中的ReentrantLock可以通過構造函數指定是否為公平鎖。默認情況下,ReentrantLock是非公平鎖。synchronized也是一種非公平鎖。
公平鎖的優點是避免了“饑餓”現象,每個線程都有機會獲得鎖。缺點是效率相對較低,因為需要維護一個等待隊列,并且線程切換的開銷也比較大。非公平鎖的優點是效率高,吞吐量大,但缺點是可能導致某些線程長時間無法獲取到鎖,產生“饑餓”現象。
實際應用中,如果對公平性要求不高,優先選擇非公平鎖,以提高性能。如果需要保證每個線程都有機會執行,避免“饑餓”,則選擇公平鎖。
可重入鎖與不可重入鎖:理解鎖的嵌套使用
可重入鎖是指,當一個線程已經獲取到鎖之后,允許它再次獲取該鎖,而不需要釋放之前的鎖。這意味著同一個線程可以多次進入被該鎖保護的代碼塊。
Java中的synchronized和ReentrantLock都是可重入鎖。可重入鎖避免了死鎖的發生,例如:
public class ReentrantLockExample { ReentrantLock lock = new ReentrantLock(); public void outer() { lock.lock(); try { System.out.println("Outer method acquired lock"); inner(); } finally { lock.unlock(); System.out.println("Outer method released lock"); } } public void inner() { lock.lock(); try { System.out.println("Inner method acquired lock"); } finally { lock.unlock(); System.out.println("Inner method released lock"); } } public static void main(String[] args) { ReentrantLockExample example = new ReentrantLockExample(); example.outer(); } }
如果ReentrantLock不是可重入鎖,那么inner方法在嘗試獲取鎖時,會因為鎖已經被outer方法持有而阻塞,導致死鎖。
獨占鎖與共享鎖:控制資源訪問權限
獨占鎖(也稱為互斥鎖)是指一次只能被一個線程持有的鎖。當一個線程獲取了獨占鎖之后,其他線程必須等待該線程釋放鎖才能獲取。Java中的synchronized和ReentrantLock都是獨占鎖。
共享鎖是指可以被多個線程同時持有的鎖。多個線程可以并發地讀取被共享鎖保護的資源,但如果需要修改資源,則必須獲取獨占鎖。Java中的ReadWriteLock接口提供了讀寫鎖的實現,讀鎖是共享鎖,寫鎖是獨占鎖。
讀寫鎖適用于讀多寫少的場景,可以提高程序的并發性能。多個線程可以同時讀取數據,只有在寫數據時才需要進行互斥。
深入理解自旋鎖:優化短時間鎖競爭場景
自旋鎖是一種特殊的鎖,它不會讓線程進入阻塞狀態,而是讓線程循環等待鎖的釋放。當鎖被其他線程持有時,嘗試獲取鎖的線程會不斷地循環檢查鎖是否可用,直到獲取到鎖為止。
自旋鎖適用于鎖的持有時間非常短的場景。如果鎖的持有時間較長,自旋鎖會導致線程一直占用CPU資源,造成性能浪費。
Java中并沒有直接提供自旋鎖的實現,但可以使用CAS算法來實現簡單的自旋鎖。例如:
public class SpinLock { private AtomicReference<Thread> owner = new AtomicReference<>(); public void lock() { Thread current = Thread.currentThread(); while (!owner.compareAndSet(null, current)) { // 自旋等待 } } public void unlock() { Thread current = Thread.currentThread(); owner.compareAndSet(current, null); } }
CAS算法的性能取決于CPU的架構和鎖競爭的激烈程度。在高并發場景下,CAS算法可能會出現大量的沖突,導致性能下降。
鎖升級:從偏向鎖到重量級鎖
為了提高鎖的性能,JVM對鎖進行了優化,引入了鎖升級的概念。鎖升級是指鎖的狀態會根據鎖競爭的激烈程度進行升級,從低級別的鎖到高級別的鎖。
鎖的級別從低到高依次為:偏向鎖、輕量級鎖、重量級鎖。
- 偏向鎖:當一個線程訪問同步塊并獲取鎖時,會在對象頭中記錄該線程的ID。以后該線程再次進入同步塊時,不需要進行任何同步操作,直接可以獲取鎖。偏向鎖適用于只有一個線程訪問同步塊的場景。
- 輕量級鎖:當多個線程嘗試獲取同一個鎖時,偏向鎖會升級為輕量級鎖。輕量級鎖使用CAS算法來嘗試獲取鎖,如果獲取失敗,則會自旋等待。輕量級鎖適用于鎖競爭不激烈的場景。
- 重量級鎖:當輕量級鎖自旋等待一定次數后仍然無法獲取鎖,或者有其他線程已經持有鎖時,輕量級鎖會升級為重量級鎖。重量級鎖會讓線程進入阻塞狀態,等待鎖的釋放。重量級鎖適用于鎖競爭激烈的場景。
理解鎖升級的過程,可以幫助我們更好地理解JVM的鎖優化機制,并根據實際場景選擇合適的鎖。