Java中的ReentrantLock、ReentrantReadWriteLock、StampedLock詳解
1. ReentrantReadWriteLock
本章路線總綱 無鎖→獨占鎖→讀寫鎖→郵戳鎖
關(guān)于鎖的大廠面試題 你知道Java里面有哪些鎖? 你說你用過讀寫鎖,鎖饑餓問題是什么?有沒有比讀寫鎖更快的鎖? StampedLock知道嗎?(郵戳鎖/票據(jù)鎖) ReentrantReadWriteLock有鎖降級機制,你知道嗎?
1.1 讀寫鎖ReentrantReadWriteLock
讀寫鎖:一個資源能夠被多個讀線程訪問,或者被一個寫線程訪問但是不能同時存在讀寫線程。
它只允許讀讀共存,而讀寫和寫寫依然是互斥的,大多實際場景是“讀/讀”線程間并不存在互斥關(guān)系,只有"讀/寫"線程或"寫/寫"線程間的操作需要互斥的。因此引入ReentrantReadWriteLock 一個ReentrantReadWriteLock同時只能存在一個寫鎖但是可以存在多個讀鎖,但不能同時存在寫鎖和讀鎖(切菜還是拍蒜選一個)。也即一個資源可以被多個讀操作訪問―或一個寫操作訪問,但兩者不能同時進(jìn)行。只有在讀多寫少情景下,讀寫鎖才具有較高的性能體現(xiàn)。
1.2 鎖降級
寫鎖的降級,降級成為了讀鎖
1)如果同一個線程持有了寫鎖,在沒有釋放寫鎖的情況下,它還可以繼續(xù)獲得讀鎖。這就是寫鎖的降級,降級成為了讀鎖。
2)規(guī)則慣例,先獲取寫鎖,然后獲取讀鎖,再釋放寫鎖的次序。
3)如果釋放了寫鎖,那么就完全轉(zhuǎn)換為讀鎖。
鎖降級是為了讓當(dāng)前線程感知到數(shù)據(jù)的變化,目的是保證數(shù)據(jù)可見性
如果有線程在讀,那么寫線程是無法獲取寫鎖的,是悲觀鎖的策略
在ReentrantReadWriteLock中,當(dāng)讀鎖被使用時,如果有線程嘗試獲取寫鎖,該寫線程會被阻塞。所以,需要釋放所有讀鎖,才可獲取寫鎖。
寫鎖和讀鎖是互斥的(這里的互斥是指線程間的互斥,當(dāng)前線程可以獲取到寫鎖又獲取到讀鎖,但是獲取到了讀鎖不能繼續(xù)獲取寫鎖),這是因為讀寫鎖要保持寫操作的可見性。因為,如果允許讀鎖在被獲取的情況下對寫鎖的獲取,那么正在運行的其他讀線程無法感知到當(dāng)前寫線程的操作。
ReentrantReadWriteLock讀的過程中不允許寫,只有等待線程都釋放了讀鎖,當(dāng)前線程才能獲取寫鎖,也就是寫入必須等待,這是一種悲觀的讀鎖,人家還在讀著那,你先別去寫,省的數(shù)據(jù)亂。
分析StampedLock(后面詳細(xì)講解),會發(fā)現(xiàn)它改進(jìn)之處在于: 讀的過程中也允許獲取寫鎖介入(相當(dāng)牛B,讀和寫兩個操作也讓你“共享”(注意引號)),這樣會導(dǎo)致我們讀的數(shù)據(jù)就可能不一致所以,需要額外的方法來判斷讀的過程中是否有寫入,這是一種樂觀的讀鎖。 顯然樂觀鎖的并發(fā)效率更高,但一旦有小概率的寫入導(dǎo)致讀取的數(shù)據(jù)不一致,需要能檢測出來,再讀一遍就行。
1.3 為什么要鎖降級?
鎖降級確實不太貼切,明明是鎖切換,在寫鎖釋放前由寫鎖切換成了讀鎖。問題的關(guān)鍵其實是為什么要在鎖切換前就加上讀鎖呢?防止釋放寫鎖的瞬間被其他線程拿到寫鎖然后修改了數(shù)據(jù),然后本線程在拿到讀鎖后讀取數(shù)據(jù)就發(fā)生了錯亂。但是,我把鎖的范圍加大一點不就行了嗎?在寫鎖的范圍里面完成讀鎖里面要干的事。缺點呢就是延長了寫鎖的占用時長,導(dǎo)致性能下降。對于中小公司而言沒必要,隨便在哪都能把這點性能撿回來了!
1.4 鎖饑餓問題
ReentrantReadWriteLock實現(xiàn)了讀寫分離,但是一旦讀操作比較多的時候,想要獲取寫鎖就變得比較困難了,假如當(dāng)前1000個線程,999個讀,1個寫,有可能999個讀取線程長時間搶到了鎖,那1個寫線程就悲劇了因為當(dāng)前有可能會一直存在讀鎖,而無法獲得寫鎖,根本沒機會寫。
如何緩解鎖饑餓問題? 使用"公平"策略可以一定程度上緩解這個問題,但是"公平"策略是以犧牲系統(tǒng)吞吐量為代價的
StampedLock類的樂觀讀鎖閃亮登場
2. 郵戳鎖StampedLock
2.1 StampedLock橫空出世
StampedLock(也叫票據(jù)鎖)是JDK1.8中新增的一個讀寫鎖,也是對JDK1.5中的讀寫鎖ReentrantReadWriteLock的優(yōu)化。
stamp(戳記,long類型) 代表了鎖的狀態(tài)。當(dāng)stamp返回零時,表示線程獲取鎖失敗。并且,當(dāng)釋放鎖或者轉(zhuǎn)換鎖的時候,需要傳入最初獲取的stamp值。
ReentrantReadWriteLock的讀鎖被占用的時候,其他線程嘗試獲取寫鎖的時候會被阻塞。但是,StampedLock采取樂觀獲取鎖后,其他線程嘗試獲取寫鎖時不會被阻塞,這其實是對讀鎖的優(yōu)化,所以,在獲取樂觀讀鎖后,還需要對結(jié)果進(jìn)行校驗。
ReentrantReadWriteLock 允許多個線程向時讀,但是只允許一個線程寫,在線程獲取到寫鎖的時候,其他寫操作和讀操作都會處于阻塞狀態(tài), 讀鎖和寫鎖也是互斥的,所以在讀的時候是不允許寫的,讀寫鎖比傳統(tǒng)的synchronized速度要快很多,原因就是在于ReentrantReadWriteLock支持讀并發(fā),讀讀可以共享。
對短的只讀代碼段,使用樂觀模式通??梢詼p少爭用并提高吞吐量
2.2 ReentrantLock、ReentrantReadWriteLock、StampedLock性能比較
public class ReentrantReadWriteLockTest { static Lock lock = new ReentrantLock(); static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); static StampedLock stampedLock = new StampedLock(); static int read = 1000; static int write = 3; static long mills = 10; public static void main(String[] args) { testReentrantLock(); testReentrantReadWriteLock(); // System.out.println("========================="); testStampedLock(); } public static void testStampedLock() { ExecutorService executorService = Executors.newFixedThreadPool(100); ExecutorService executorServiceWrite = Executors.newFixedThreadPool(3); CountDownLatch latch = new CountDownLatch(read + write); long l = System.currentTimeMillis(); for (int i = 0; i < read; i++) { executorService.execute(() -> { // tryOptimisticRead(); readStampedLock(); latch.countDown(); }); } for (int i = 0; i < write; i++) { executorServiceWrite.execute(() -> { writeStampedLock(); latch.countDown(); // System.out.println("時間間隔:"+(System.currentTimeMillis()-l)); }); } try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } executorService.shutdown(); executorServiceWrite.shutdown(); System.out.println("testStampedLock執(zhí)行耗時:" + (System.currentTimeMillis() - l)); } public static void testReentrantLock() { ExecutorService executorService = Executors.newFixedThreadPool(100); ExecutorService executorServiceWrite = Executors.newFixedThreadPool(3); CountDownLatch latch = new CountDownLatch(read + write); long l = System.currentTimeMillis(); for (int i = 0; i < read; i++) { executorService.execute(() -> { read(); latch.countDown(); }); } for (int i = 0; i < write; i++) { executorServiceWrite.execute(() -> { write(); latch.countDown(); }); } try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } executorService.shutdown(); executorServiceWrite.shutdown(); System.out.println("testReentrantLock執(zhí)行耗時:" + (System.currentTimeMillis() - l)); } public static void testReentrantReadWriteLock() { ExecutorService executorService = Executors.newFixedThreadPool(100); ExecutorService executorServiceWrite = Executors.newFixedThreadPool(3); CountDownLatch latch = new CountDownLatch(read + write); long l = System.currentTimeMillis(); for (int i = 0; i < read; i++) { executorService.execute(() -> { readLock(); latch.countDown(); }); } for (int i = 0; i < write; i++) { executorServiceWrite.execute(() -> { writeLock(); latch.countDown(); // System.out.println("時間間隔:"+(System.currentTimeMillis()-l)); }); } try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } executorService.shutdown(); executorServiceWrite.shutdown(); System.out.println("testReentrantReadWriteLock執(zhí)行耗時:" + (System.currentTimeMillis() - l)); } public static void tryOptimisticRead() { long stamp = stampedLock.tryOptimisticRead(); try { Thread.sleep(mills); if (!stampedLock.validate(stamp)) { long readLock = stampedLock.readLock(); try { } finally { stampedLock.unlock(readLock); } } } catch (InterruptedException e) { e.printStackTrace(); } } public static void readStampedLock() { long stamp = stampedLock.readLock(); try { Thread.sleep(mills); } catch (InterruptedException e) { e.printStackTrace(); } finally { stampedLock.unlock(stamp); } } public static void writeStampedLock() { long stamp = stampedLock.writeLock(); try { Thread.sleep(mills); } catch (InterruptedException e) { e.printStackTrace(); } finally { stampedLock.unlock(stamp); } } public static void readLock() { readWriteLock.readLock().lock(); try { Thread.sleep(mills); } catch (InterruptedException e) { e.printStackTrace(); } finally { readWriteLock.readLock().unlock(); } } public static void writeLock() { readWriteLock.writeLock().lock(); try { Thread.sleep(mills); } catch (InterruptedException e) { e.printStackTrace(); } finally { readWriteLock.writeLock().unlock(); } } public static void read() { lock.lock(); try { Thread.sleep(mills); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public static void write() { lock.lock(); try { Thread.sleep(mills); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } }
執(zhí)行結(jié)果
testReentrantLock執(zhí)行耗時:15868
testReentrantReadWriteLock執(zhí)行耗時:218
testStampedLock執(zhí)行耗時:221
根據(jù)執(zhí)行結(jié)果可以明顯看出在讀多寫少的情況下,ReentrantLock的性能是比較差的,而ReentrantReadWriteLock和StampedLock性能差不多相同,而StampedLock主要是為了解決ReentrantReadWriteLock可能出現(xiàn)的鎖饑餓問題。
2.3 StampedLock總結(jié)
StampedLock的特點 所有獲取鎖的方法,都返回一個郵戳( Stamp) , Stamp為零表示獲取失敗,其余都表示成功; 所有釋放鎖的方法,都需要一個郵戳(Stamp),這個Stamp必須是和成功獲取鎖時得到的Stamp一致; StampedLock是不可重入的,危險(如果一個線程已經(jīng)持有了寫鎖,再去獲取寫鎖的話就會造成死鎖)
StampedLock有三種訪問模式 Reading (讀模式悲觀):功能和ReentrantReadWriteLock的讀鎖類似 Writing(寫模式):功能和ReentrantRedWriteLock的寫鎖類似 Optimistic reading(樂觀讀模式):無鎖機制,類似于數(shù)據(jù)庫中的樂觀鎖,支持讀寫并發(fā),很樂觀認(rèn)對為讀取時沒人修改,假如被修改再實現(xiàn)升級為悲觀讀模式
主要API tryOptimisticRead():加樂觀讀鎖 validate(long stamp):校驗樂觀讀鎖執(zhí)行過程中有無寫鎖攪局
StampedLock的缺點 StampedLock 不支持重入,沒有Re開頭 StampedLock的悲觀讀鎖和寫鎖都不支持條件變量(Condition),這個也需要注意。 使用StampedLock一定不要調(diào)用中斷操作,即不要調(diào)用interrupt()方法
有關(guān)鎖的知識都學(xué)習(xí)完了,其實挺雞肋的,工作直接用分布式鎖,幾乎不會用到JVM層級的鎖,但是面試要問,不得不學(xué)!
到此這篇關(guān)于Java中的ReentrantLock、ReentrantReadWriteLock、StampedLock詳解的文章就介紹到這了,更多相關(guān)ReentrantLock、ReentrantReadWriteLock、StampedLock內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決idea導(dǎo)入maven項目缺少jar包的問題方法
這篇文章主要介紹了解決idea導(dǎo)入maven項目缺少jar包的問題,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06Mybatis中通用Mapper的InsertList()用法
文章介紹了通用Mapper中的insertList()方法在批量新增時的使用方式,包括自增ID和自定義ID的情況,對于自增ID,使用tk.mybatis.mapper.additional.insert.InsertListMapper包下的insertList()方法;對于自定義ID,需要重寫insertList()方法2025-02-02