編程:Lock鎖與synchronized的深度對比與應用)
1. 為什么我們需要Lock鎖在Java并發(fā)編程的世界里synchronized關(guān)鍵字可能是大多數(shù)開發(fā)者最先接觸的線程同步機制。但當你開始構(gòu)建更復雜的并發(fā)系統(tǒng)時很快就會發(fā)現(xiàn)synchronized存在一些局限性。這就是為什么Java 5引入了java.util.concurrent.locks包其中Lock接口及其實現(xiàn)類提供了更靈活的線程同步控制。我清楚地記得第一次遇到synchronized不夠用的場景當時需要實現(xiàn)一個帶有超時機制的鎖獲取操作。使用synchronized時如果線程無法立即獲取鎖它會一直阻塞等待沒有超時選項。而Lock接口的tryLock(long time, TimeUnit unit)方法完美解決了這個問題。2. Lock接口的核心能力解析2.1 Lock與synchronized的關(guān)鍵區(qū)別Lock接口提供了比synchronized更豐富的功能集。最顯著的區(qū)別包括可中斷的鎖獲取lockInterruptibly()方法允許在等待鎖的過程中響應中斷嘗試獲取鎖tryLock()方法可以立即返回獲取鎖的結(jié)果而不阻塞公平鎖選項某些實現(xiàn)支持公平鎖按照請求順序分配鎖多個條件變量一個Lock可以關(guān)聯(lián)多個Condition對象在實際項目中我發(fā)現(xiàn)這些特性特別有用。比如在實現(xiàn)一個連接池時使用tryLock()可以優(yōu)雅地處理連接獲取超時的情況而不是讓線程無限期等待。2.2 Lock的標準用法模式使用Lock時有一個必須遵循的模式以確保鎖能被正確釋放Lock lock new ReentrantLock(); lock.lock(); try { // 臨界區(qū)代碼 } finally { lock.unlock(); }這個模式中將unlock()放在finally塊中是關(guān)鍵。我曾經(jīng)在一個項目中看到有開發(fā)者將unlock()放在try塊中當臨界區(qū)代碼拋出異常時鎖就無法釋放導致整個系統(tǒng)最終死鎖。3. ReentrantLock深度剖析3.1 可重入性實現(xiàn)原理ReentrantLock是Lock接口的標準實現(xiàn)它支持可重入鎖。這意味著一個線程可以多次獲取同一個鎖而不會導致死鎖。內(nèi)部通過一個計數(shù)器跟蹤鎖的獲取次數(shù)每次lock()調(diào)用計數(shù)器加1每次unlock()調(diào)用計數(shù)器減1。我曾經(jīng)在一個遞歸算法中使用ReentrantLock算法會在遞歸調(diào)用中多次進入臨界區(qū)。如果使用不可重入鎖線程會在第二次嘗試獲取鎖時阻塞自己導致死鎖。3.2 公平鎖與非公平鎖ReentrantLock提供了公平性選項// 公平鎖 Lock fairLock new ReentrantLock(true); // 非公平鎖 Lock unfairLock new ReentrantLock(false);公平鎖保證等待時間最長的線程優(yōu)先獲取鎖但會帶來性能開銷。在大多數(shù)情況下非公平鎖的性能更好因為減少了線程切換的開銷。只有在嚴格要求公平性的場景下才應該使用公平鎖。4. 讀寫鎖(ReadWriteLock)的應用4.1 讀寫鎖的使用場景ReadWriteLock接口及其實現(xiàn)ReentrantReadWriteLock提供了一種特殊的鎖允許多個讀操作同時進行但寫操作是獨占的。這在讀多寫少的場景下可以顯著提高性能。ReadWriteLock rwLock new ReentrantReadWriteLock(); Lock readLock rwLock.readLock(); Lock writeLock rwLock.writeLock(); // 讀操作 readLock.lock(); try { // 讀取共享數(shù)據(jù) } finally { readLock.unlock(); } // 寫操作 writeLock.lock(); try { // 修改共享數(shù)據(jù) } finally { writeLock.unlock(); }4.2 讀寫鎖的升級與降級一個常見的誤區(qū)是嘗試將讀鎖升級為寫鎖readLock.lock(); try { // 讀取數(shù)據(jù) writeLock.lock(); // 這會死鎖 try { // 修改數(shù)據(jù) } finally { writeLock.unlock(); } } finally { readLock.unlock(); }這種寫法會導致死鎖因為寫鎖的獲取需要等待所有讀鎖釋放包括當前線程持有的讀鎖。正確的做法是先釋放讀鎖再獲取寫鎖。不過鎖降級寫鎖降級為讀鎖是安全的writeLock.lock(); try { // 修改數(shù)據(jù) readLock.lock(); // 鎖降級 try { writeLock.unlock(); // 保持讀鎖 // 讀取數(shù)據(jù) } finally { readLock.unlock(); } } catch (Exception e) { writeLock.unlock(); }5. Condition變量的高級用法5.1 Condition與Object監(jiān)視器方法的對比Condition接口提供了類似Object.wait()和notify()的功能但更靈活。一個Lock可以創(chuàng)建多個Condition對象允許更精確的線程通知控制。Lock lock new ReentrantLock(); Condition notEmpty lock.newCondition(); Condition notFull lock.newCondition(); // 生產(chǎn)者線程 lock.lock(); try { while (buffer.isFull()) { notFull.await(); } buffer.add(item); notEmpty.signal(); } finally { lock.unlock(); } // 消費者線程 lock.lock(); try { while (buffer.isEmpty()) { notEmpty.await(); } item buffer.remove(); notFull.signal(); } finally { lock.unlock(); }5.2 使用Condition實現(xiàn)精確喚醒與Object.notifyAll()不同Condition.signal()可以精確喚醒等待在特定條件上的線程。這在實現(xiàn)復雜同步邏輯時非常有用。我曾經(jīng)用這個特性實現(xiàn)了一個高效的任務調(diào)度器不同類型的任務等待在不同的Condition上調(diào)度器可以根據(jù)任務類型精確喚醒對應的線程。6. 性能考量與最佳實踐6.1 鎖粒度的選擇鎖的粒度選擇對性能有重大影響。一般來說粗粒度鎖簡單但并發(fā)度低細粒度鎖復雜但并發(fā)度高在高度競爭的場景下細粒度鎖通常表現(xiàn)更好。但要注意避免死鎖確保鎖的獲取順序一致。6.2 避免常見陷阱忘記釋放鎖總是使用try-finally塊確保鎖釋放鎖泄露確保異常情況下鎖能被釋放嵌套鎖注意獲取多個鎖時的順序避免死鎖長時間持有鎖盡量減少臨界區(qū)代碼的執(zhí)行時間我曾經(jīng)調(diào)試過一個性能問題發(fā)現(xiàn)是因為在臨界區(qū)內(nèi)執(zhí)行了數(shù)據(jù)庫查詢操作。將數(shù)據(jù)庫查詢移到臨界區(qū)外性能立即提升了10倍。7. Lock與synchronized的選擇指南雖然Lock更強大但synchronized仍然有其優(yōu)勢簡單性語法更簡潔自動釋放退出同步塊時自動釋放鎖JVM優(yōu)化JVM對synchronized有特殊優(yōu)化選擇原則需要高級功能如超時、中斷等時使用Lock簡單同步場景使用synchronized讀多寫少場景使用ReadWriteLock在實際項目中我通常會在性能關(guān)鍵路徑上使用Lock而在簡單的輔助代碼中使用synchronized。8. 實戰(zhàn)案例實現(xiàn)一個簡單的線程安全緩存讓我們用ReentrantReadWriteLock實現(xiàn)一個線程安全的緩存public class ThreadSafeCacheK, V { private final MapK, V map new HashMap(); private final ReadWriteLock rwLock new ReentrantReadWriteLock(); private final Lock readLock rwLock.readLock(); private final Lock writeLock rwLock.writeLock(); public V get(K key) { readLock.lock(); try { return map.get(key); } finally { readLock.unlock(); } } public void put(K key, V value) { writeLock.lock(); try { map.put(key, value); } finally { writeLock.unlock(); } } public V computeIfAbsent(K key, FunctionK, V mappingFunction) { V value get(key); if (value null) { writeLock.lock(); try { // 雙重檢查因為可能有其他線程已經(jīng)修改了 value map.get(key); if (value null) { value mappingFunction.apply(key); map.put(key, value); } } finally { writeLock.unlock(); } } return value; } }這個實現(xiàn)展示了讀寫鎖的典型用法以及如何在computeIfAbsent方法中處理先讀后寫的場景。注意writeLock.lock()調(diào)用會阻塞所有讀鎖和寫鎖所以我們在獲取寫鎖前先釋放了讀鎖。9. 鎖的性能測試與對比為了直觀理解不同鎖實現(xiàn)的性能差異我設計了一個簡單的基準測試BenchmarkMode(Mode.Throughput) OutputTimeUnit(TimeUnit.SECONDS) public class LockBenchmark { State(Scope.Thread) public static class MyState { public final Lock lock new ReentrantLock(); public final Object syncLock new Object(); public int counter; } Benchmark public void testReentrantLock(MyState state) { state.lock.lock(); try { state.counter; } finally { state.lock.unlock(); } } Benchmark public void testSynchronized(MyState state) { synchronized (state.syncLock) { state.counter; } } }在4核機器上運行這個基準測試結(jié)果可能顯示低競爭情況下synchronized可能更快因為JVM有優(yōu)化高競爭情況下ReentrantLock通常表現(xiàn)更好特別是使用tryLock時但要注意實際性能取決于具體場景和JVM實現(xiàn)。我在生產(chǎn)環(huán)境中見過synchronized和ReentrantLock性能差異達到30%的情況。10. 鎖的調(diào)試與問題診斷10.1 檢測死鎖JDK提供了幾種檢測死鎖的方法jstack工具可以顯示線程轉(zhuǎn)儲和鎖持有情況ThreadMXBean編程方式檢測死鎖ThreadMXBean bean ManagementFactory.getThreadMXBean(); long[] threadIds bean.findDeadlockedThreads(); if (threadIds ! null) { ThreadInfo[] infos bean.getThreadInfo(threadIds); for (ThreadInfo info : infos) { System.out.println(info); } }10.2 鎖爭用診斷高鎖爭用會嚴重影響性能??梢允褂肑FR(Java Flight Recorder)或商業(yè)APM工具監(jiān)控鎖爭用情況。我曾經(jīng)通過分析鎖爭用情況將一個關(guān)鍵組件的吞吐量提高了5倍方法是減小鎖粒度并縮短臨界區(qū)。11. Java并發(fā)工具包的演進Java的并發(fā)工具包在不斷演進。值得關(guān)注的新特性包括StampedLockJava 8引入的樂觀讀鎖VarHandleJava 9引入的低級別內(nèi)存操作虛擬線程Java 19引入的輕量級線程特別是StampedLock在某些讀多寫少的場景下比ReadWriteLock性能更好StampedLock lock new StampedLock(); // 樂觀讀 long stamp lock.tryOptimisticRead(); // 讀取共享變量 if (!lock.validate(stamp)) { // 樂觀讀失敗獲取悲觀讀鎖 stamp lock.readLock(); try { // 再次讀取 } finally { lock.unlockRead(stamp); } }12. 個人經(jīng)驗分享在多年使用Java并發(fā)工具的經(jīng)驗中我總結(jié)了以下幾點心得優(yōu)先考慮并發(fā)工具類對于常見模式(如生產(chǎn)者-消費者)優(yōu)先考慮使用java.util.concurrent中的現(xiàn)成工具類而不是自己基于鎖實現(xiàn)。避免過早優(yōu)化先用簡單的synchronized實現(xiàn)功能當性能測試表明需要更高級功能時再使用Lock。編寫可測試的并發(fā)代碼將并發(fā)控制邏輯與業(yè)務邏輯分離便于單元測試。重視代碼可讀性復雜的鎖邏輯很難維護適當添加注釋說明鎖的用途和獲取順序??紤]替代方案有時候無鎖數(shù)據(jù)結(jié)構(gòu)或Actor模型可能是更好的選擇。我曾在重構(gòu)一個高并發(fā)交易系統(tǒng)時將復雜的鎖邏輯替換為ConcurrentHashMap和Atomic變量不僅性能提升了代碼也更容易理解和維護。