面試官:瞭解鎖嗎?java
小明:瞭解,還常常用過。mysql
面試官:說說synchronized和lock的區別吧面試
小明:synchronized是可重入鎖,因爲lock是一個接口,重入性取決於實現,synchronized不支持中斷,而lock能夠……spring
面試官:好了,那有沒有比這兩種鎖更快的鎖呢?sql
小明:在讀多寫少的狀況下,讀寫鎖比他們的效率更高。數據庫
面試官:那有沒有比讀寫鎖更快的鎖呢?編程
小明:……設計模式
我靠,問的這麼深的嗎?小明當時就矇蔽了,由於它項目中使用比較多的就是synchronized,讀寫鎖都不多用到,由於不多牽扯到多線程問題,這個面試讓他知道了多線程的重要性。緩存
讀寫鎖:容許多個線程同時讀,可是隻容許一個線程寫,在線程獲取到寫鎖的時候,其餘寫操做和讀操做都會處於阻塞狀態,讀鎖和寫鎖也是互斥的,因此在讀的時候是不容許寫的,那如何實現一個讀寫鎖呢?安全
讀寫鎖比傳統的synchronized速度要快不少,緣由就是在於讀寫鎖支持讀併發,而synchronized要求全部操做都是串行化,舉個例子,我須要查詢某個用戶的基本信息,這些信息不多發生變化,因此咱們會將這部分信息存放到緩存中,咱們的查詢操做爲:
按照上面流程圖,若是使用synchronized的時候,查詢緩存都會阻塞,可是使用讀寫鎖,查詢緩存時併發的,查詢數據庫是阻塞的,因此,讀寫鎖在讀多寫少的狀況下,性能明顯要優於synchronized。
人類的文明在進步,java也在進步,對知識的渴望也在不斷的增長,因此咱們就不斷的在想這麼一個問題,讀寫鎖的讀和寫是互斥,那咱們能不能作到讀和寫支持併發呢?
StampedLock實際上是對讀寫鎖的一種改進,它支持在讀同時進行一個寫操做,也就是說,它的性能將會比讀寫鎖更快。
更通俗的講就是在讀鎖沒有釋放的時候是能夠獲取到一個寫鎖,獲取到寫鎖以後,讀鎖阻塞,這一點和讀寫鎖一致,惟一的區別在於讀寫鎖不支持在沒有釋放讀鎖的時候獲取寫鎖。
悲觀讀:與讀寫鎖的讀寫相似,容許多個線程獲取悲觀讀鎖
寫鎖:與讀寫鎖的寫鎖相似,寫鎖和悲觀讀是互斥的。
樂觀讀:無鎖機制,相似於數據庫中的樂觀鎖,它支持在不是放寫鎖的時候是能夠獲取到一個寫鎖的,這點和讀寫鎖不一樣。
咱們先來看看悲觀讀於與寫鎖的基本語法
//獲取悲觀讀 long stamp = lock.readLock(); try{ String info = mapCache.get(name); if(null != info){ return info; } }finally { //釋放悲觀讀 lock.unlock(stamp); } //獲取寫鎖 stamp = lock.writeLock(); try{ //判斷一下緩存中是否被插入了數據 String info = mapCache.get(name); if(null != info){ return info; } //這裏是往數據庫獲取數據 String infoByDb = mapDb.get(name); //將數據插入緩存 mapCache.put(name,infoByDb); }finally { //釋放寫鎖 lock.unlock(stamp); }
咱們看到,StampedLock語法和讀寫鎖ReentrantReadWriteLock有了一點點區別,
獲取鎖的返回值:
StampedLock:long
ReentrantReadWriteLock:Lock
釋放鎖的方式:
StampedLock:unlock(stamp),須要傳入獲取鎖返回的那個long值。
ReentrantReadWriteLock:unlock(),直接調用unlock方法便可。
package com.ymy.test; import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.StampedLock; public class StampedLockTest { private static final StampedLock lock = new StampedLock(); //緩存中存儲的數據 private static Map<String,String> mapCache = new HashMap<String, String>(); //模擬數據庫存儲的數據 private static Map<String,String> mapDb = new HashMap<String, String>(); static { mapDb.put("zhangsan","你好,我是張三"); mapDb.put("sili","你好,我是李四"); } private static String getInfo(String name){ //獲取悲觀讀 long stamp = lock.readLock(); try{ String info = mapCache.get(name); if(null != info){ System.out.println("在緩存中獲取到了數據"); return info; } }finally { //釋放悲觀讀 lock.unlock(stamp); } //獲取寫鎖 stamp = lock.writeLock(); try{ //判斷一下緩存中是否被插入了數據 String info = mapCache.get(name); if(null != info){ System.out.println("獲取到了寫鎖,再次確認在緩存中獲取到了數據"); return info; } //這裏是往數據庫獲取數據 String infoByDb = mapDb.get(name); //講數據插入緩存 mapCache.put(name,infoByDb); System.out.println("緩存中沒有數據,在數據庫獲取到了數據"); }finally { //釋放寫鎖 lock.unlock(stamp); } return null; } public static void main(String[] args) { //線程1 Thread t1 = new Thread(() ->{ getInfo("zhangsan"); }); //線程2 Thread t2 = new Thread(() ->{ getInfo("zhangsan"); }); //線程啓動 t1.start(); t2.start(); //線程同步 try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } } }
這是悲觀讀+寫鎖的使用方式,達到的效果與讀寫鎖(ReentrantReadWriteLock) 是同樣的。
咱們一塊兒來驗證一下,我將代碼稍微作了一點改動,打印了兩個線程的執行日誌,同時當調用線程是zhangsan的時候休眠三秒,目的是爲了看lisi的線程可否成功的獲取到寫鎖,代碼以下:
package com.ymy.test; import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.StampedLock; import java.util.logging.Logger; public class StampedLockTest { private static Logger log = Logger.getLogger(StampedLockTest.class.getName()); private static final StampedLock lock = new StampedLock(); //緩存中存儲的數據 private static Map<String,String> mapCache = new HashMap<String, String>(); //模擬數據庫存儲的數據 private static Map<String,String> mapDb = new HashMap<String, String>(); static { mapDb.put("zhangsan","你好,我是張三"); mapDb.put("sili","你好,我是李四"); } private static String getInfo(String name){ //獲取悲觀讀 long stamp = lock.readLock(); log.info("線程名:"+Thread.currentThread().getName()+" 獲取了悲觀讀鎖" +" 用戶名:"+name); try{ if("zhangsan".equals(name)){ log.info("線程名:"+Thread.currentThread().getName()+" 休眠中" +" 用戶名:"+name); Thread.sleep(3000); log.info("線程名:"+Thread.currentThread().getName()+" 休眠結束" +" 用戶名:"+name); } String info = mapCache.get(name); if(null != info){ log.info("在緩存中獲取到了數據"); return info; } } catch (InterruptedException e) { log.info("線程名:"+Thread.currentThread().getName()+" 釋放了悲觀讀鎖"); e.printStackTrace(); } finally { //釋放悲觀讀 lock.unlock(stamp); } //獲取寫鎖 stamp = lock.writeLock(); log.info("線程名:"+Thread.currentThread().getName()+" 獲取了寫鎖" +" 用戶名:"+name); try{ //判斷一下緩存中是否被插入了數據 String info = mapCache.get(name); if(null != info){ log.info("獲取到了寫鎖,再次確認在緩存中獲取到了數據"); return info; } //這裏是往數據庫獲取數據 String infoByDb = mapDb.get(name); //講數據插入緩存 mapCache.put(name,infoByDb); log.info("緩存中沒有數據,在數據庫獲取到了數據"); }finally { //釋放寫鎖 log.info("線程名:"+Thread.currentThread().getName()+" 釋放了寫鎖" +" 用戶名:"+name); lock.unlock(stamp); } return null; } public static void main(String[] args) { //線程1 Thread t1 = new Thread(() ->{ getInfo("zhangsan"); }); //線程2 Thread t2 = new Thread(() ->{ getInfo("lisi"); }); //線程啓動 t1.start(); t2.start(); //線程同步 try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } } }
若是在zhansan的線程休眠階段李四的線程獲取到了寫鎖,那麼表明悲觀讀和寫鎖不是互斥的,反之互斥,請看代碼運行結果:
三月 29, 2020 11:30:58 上午 com.ymy.test.StampedLockTest getInfo 信息: 線程名:Thread-2 獲取了悲觀讀鎖 用戶名:lisi 三月 29, 2020 11:30:58 上午 com.ymy.test.StampedLockTest getInfo 信息: 線程名:Thread-1 獲取了悲觀讀鎖 用戶名:zhangsan 三月 29, 2020 11:30:58 上午 com.ymy.test.StampedLockTest getInfo 信息: 線程名:Thread-1 休眠中 用戶名:zhangsan 三月 29, 2020 11:31:01 上午 com.ymy.test.StampedLockTest getInfo 信息: 線程名:Thread-1 休眠結束 用戶名:zhangsan 三月 29, 2020 11:31:01 上午 com.ymy.test.StampedLockTest getInfo 信息: 線程名:Thread-1 獲取了寫鎖 用戶名:zhangsan 三月 29, 2020 11:31:01 上午 com.ymy.test.StampedLockTest getInfo 信息: 緩存中沒有數據,在數據庫獲取到了數據 三月 29, 2020 11:31:01 上午 com.ymy.test.StampedLockTest getInfo 信息: 線程名:Thread-1 釋放了寫鎖 用戶名:zhangsan 三月 29, 2020 11:31:01 上午 com.ymy.test.StampedLockTest getInfo 信息: 線程名:Thread-2 獲取了寫鎖 用戶名:lisi 三月 29, 2020 11:31:01 上午 com.ymy.test.StampedLockTest getInfo 信息: 緩存中沒有數據,在數據庫獲取到了數據 三月 29, 2020 11:31:01 上午 com.ymy.test.StampedLockTest getInfo 信息: 線程名:Thread-2 釋放了寫鎖 用戶名:lisi
咱們仔細看打印日誌的輸出時間, 11:30:58 lisi和zhangsan都獲取到了悲觀讀鎖,而且zhangsan開始休眠,而後11:31:01的時候休眠結束,zhangsan獲取到了寫鎖,因此悲觀讀與寫鎖確定是互斥的,那這樣的效率不是和讀寫鎖同樣嗎?爲何說它比讀寫鎖更快呢?這不是矛盾嗎?
客官,別急啊,要記住精彩的永遠在最後,StampedLock特鎖模式咱們只用了其中的兩個,還有一個沒有出場呢,下面咱們來看看樂觀讀。
樂觀讀並非一種鎖,因此請不要和悲觀讀聯繫在一塊兒,它是一種無鎖機制,至關於java的原子類操做,因此理論上性能會比讀寫鎖(ReentrantReadWriteLock)更快一點,但不絕對。
當樂觀讀讀取了成員變量的時候,須要將變量賦值給局部變量,而後再判斷程序運行期間是否存在寫鎖,若是存在,升級爲悲觀讀。
咱們一塊兒來看一下樂觀讀的實現:
package com.ymy.test; import java.util.concurrent.locks.StampedLock; public class NumSumTest { private static final StampedLock lock = new StampedLock(); private static int num1 = 1; private static int num2 = 1; /** * 修改爲員變量的值,+1 * * @return */ private static int sum() { System.out.println("求和方法被執行了"); //獲取樂觀讀 long stamp = lock.tryOptimisticRead(); int cnum1 = num1; int cnum2 = num2; System.out.println("獲取到的成員變量值,cnum1:" + cnum1 + " cnum2:" + cnum2); try { //休眠3秒,目的是爲了讓其餘線程修改掉成員變量的值。 Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } //判斷在運行期間是否存在寫操做 true:不存在 false:存在 if (!lock.validate(stamp)) { System.out.println("存在寫操做!"); //存在寫鎖 //升級悲觀讀鎖 stamp = lock.readLock(); try { System.out.println("升級悲觀讀鎖"); cnum1 = num1; cnum2 = num2; System.out.println("從新獲取了成員變量的值=========== cnum1="+cnum1 +" cnum2="+cnum2); } finally { //釋放悲觀讀鎖 lock.unlock(stamp); } } return cnum1 + cnum2; } //使用寫鎖修改爲員變量的值 private static void updateNum() { long stamp = lock.writeLock(); try { num1 = 2; num2 = 2; } finally { lock.unlock(stamp); } } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { int sum = sum(); System.out.println("求和結果:" + sum); }); t1.start(); //休眠1秒,目的爲了讓線程t1能執行到獲取成員變量以後 Thread.sleep(1000); updateNum(); t1.join(); System.out.println("執行完畢"); } }
解釋代碼,定義了兩個成員變量,讓後利用t1線程去計算兩個成員變量的和,爲了能體現出樂觀讀的效果,我在sum()中休眠了3秒,目的是讓main主線程去修改掉成員變量的值,main函數中的休眠是爲了讓t1線程能準確地執行到讀取成員變量階段。
咱們來看看執行的結果:
求和方法被執行了 獲取到的成員變量值,cnum1:1 cnum2:1 存在寫操做! 升級悲觀讀鎖 從新獲取了成員變量的值=========== cnum1=2 cnum2=2 求和結果:4 執行完畢
咱們發現,t1首先讀取了兩個成員變量的值,而後發現了存在寫操做,那是由於main函數利用寫鎖修改了兩個成員變量的值,這個時候升級爲了悲觀讀,再次獲取成員變量的值,而後再計算兩個值的和,爲何要升級悲觀讀鎖呢?
由於在文章開頭的時候說過悲觀讀鎖與寫鎖互斥,悲觀讀鎖以前並行,因此樂觀讀升級到悲觀讀鎖以後再獲取一次成員變量,能夠保證再當前悲觀讀鎖中數據是線程安全的。
樂觀讀並非StampedLock的專利,有不少地方都使用到了樂觀讀,好比數據庫的樂觀鎖悲觀鎖,java併發工具的原子類工具。
數據庫悲觀鎖與樂觀鎖能夠參考:mysql:悲觀鎖與樂觀鎖
java 併發工具原子類參考:java併發編程:CAS(Compare and Swap)
使用StampedLock的注意事項
一、StampedLock屬於ReadWriteLock的子類,ReentrantReadWriteLock也是屬於ReadWriteLock的子類,大家發現他們的區別了嗎?看名字就能看出來StampedLock不支持重入鎖。
二、它適用於讀多寫少的狀況,若是不是這中狀況,請慎用,性能可能還不如synchronized。
三、StampedLock的悲觀讀鎖、寫鎖不支持條件變量。
四、千萬不能中斷阻塞的悲觀讀鎖或寫鎖,若是調用阻塞線程的interrupt(),會致使cpu飆升,若是但願StampedLock支持中斷操做,請使用readLockInterruptibly(悲觀讀鎖)與writeLockInterruptibly(寫鎖)。
在讀多寫少的狀況下推薦使用StampedLock,由於它的樂觀讀,性能比讀寫鎖提高了不少,可是再其餘應用場景中,使用它還須要慎重。
樂觀讀支持併發一個寫鎖,而悲觀讀和寫鎖互斥,因此在使用過程當中,咱們能夠先使用樂觀讀,而後判斷是否存在寫鎖。
若是存在,能夠升級悲觀讀鎖,因爲悲觀讀鎖和寫鎖的互斥性,他能保證線程的安全性問題,若是小明在平時的時候多看看個人博客的話,可能就不會被這個問題難住了。
原文連接:
https://blog.csdn.net/qq_3322...
文源網絡,僅供學習之用,若有侵權,聯繫刪除。我將面試題和答案都整理成了PDF文檔,還有一套學習資料,涵蓋Java虛擬機、spring框架、Java線程、數據結構、設計模式等等,但不只限於此。
關注公衆號【java圈子】獲取資料,還有優質文章每日送達。