並發讀寫的時候,很容易造成數據不一致的狀態
上案例,代碼如下:
public class ReadWriteLockDemo { public static void main(String[] args) { MyCache myCache = new MyCache(); for (int i = 0; i < 5; i++) { final int finali= i; new Thread(() -> { try { myCache.put(finali+"", finali+""); } catch (InterruptedException e) { e.printStackTrace(); } }, String.valueOf(i)).start(); } for (int i = 0; i < 5; i++) { final int finali= i; new Thread(() -> { try { myCache.get(finali+""); } catch (InterruptedException e) { e.printStackTrace(); } }, String.valueOf(i)).start(); } } } class MyCache { private volatile Map<String, Object> map = new HashMap<>(); public void put(String key, Object value) throws InterruptedException { System.out.println(Thread.currentThread().getName() + "\t-----寫入數據key"); Thread.sleep(3000); map.put(key, value); System.out.println(Thread.currentThread().getName() + "\t-----寫入數據成功"); } public void get(String key) throws InterruptedException { System.out.println(Thread.currentThread().getName() + "\t讀取數據key"); Thread.sleep(3000); Object result = map.get(key); System.out.println(Thread.currentThread().getName() + "\t讀取數據成功" + result); } }
運行結果如下:
我們可以看到的是在1進行寫入數據的時候,此時還沒有寫入成功,就已經對1進行了讀取操作,就像我們數據庫的原子性一樣,這里在還沒有對數據進行寫入完成就進行了讀取的操作,所以讀取的為空。
接下來我們看看加入了讀寫鎖的效果,這里只需要對MyCache進行修改:
加入ReadWriteLock
ReadWriteLock的作用:保證並發讀
- 寫鎖是獨占鎖,所謂獨占即為獨自占有,別的線程既不能獲取到該鎖的寫鎖,也不能獲取到對應的讀鎖。
- 讀鎖是共享鎖,所謂共享即是所有線程都可以共同持有該讀鎖
- 歸納與一句換:讀讀共存 讀寫不共存 寫寫不共存
代碼如下 :
class MyCache { private volatile Map<String, Object> map = new HashMap<>(); private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); public void put(String key, Object value) throws InterruptedException { readWriteLock.writeLock().lock(); //上寫鎖 try { System.out.println(Thread.currentThread().getName() + "\t-----寫入數據key"); Thread.sleep(3000); map.put(key, value); System.out.println(Thread.currentThread().getName() + "\t-----寫入數據成功"); } catch (Exception e) { e.printStackTrace(); } finally { readWriteLock.writeLock().unlock(); } } public void get(String key) throws InterruptedException { readWriteLock.readLock().lock(); //上讀鎖 try { System.out.println(Thread.currentThread().getName() + "\t讀取數據key"); Thread.sleep(3000); Object result = map.get(key); System.out.println(Thread.currentThread().getName() + "\t讀取數據成功" + result); } catch (Exception e) { e.printStackTrace(); } finally { readWriteLock.readLock().unlock(); } } }
運行結果如下:
可以看到我們對寫保持了一致性,讀保證了可並發讀,防止了在寫的時候進行了讀的操作導致的不一致性,所以這就是讀寫鎖的作用
ReadWriteLock鎖降級
鎖降級過程(當前線程):
為什么可以降級? 為什么在寫鎖釋放之前可以拿到讀鎖?
首先寫鎖是獨占的,讀鎖是共享的,然后讀寫鎖是線程間互斥的,鎖降級的前提是所有線程都希望對數據變化敏感,但是因為寫鎖只有一個,所以會發生降級。
你既然拿到寫鎖了,其他線程就沒法拿到讀鎖或者寫鎖了,你再拿讀鎖,其實不會和其他線程的寫鎖發送沖突的,因為你拿到寫鎖到寫鎖釋放這段時間其他線程是無法拿到任何鎖的。
注意以下情況不是鎖降級
如果先釋放寫鎖,再獲取讀鎖,可能在獲取之前,會有其他線程獲取到寫鎖,阻塞讀鎖的獲取,就無法感知數據變化了。所以需要先hold住寫鎖,保證數據無變化,獲取讀鎖,然后再釋放寫鎖。
其他線程在該線程釋放寫鎖之前,寫操作所做的數據更新對其他線程是不可見的。但是一旦寫鎖釋放,數據更新操作就會對其他線程可見。
思考?
即使是先釋放寫鎖,然后獲取讀鎖可能也沒有問題,只不過會可能會被其他線程的寫鎖阻塞一段時間;
但是並不意味着,隨后的這個讀操作看不到之前別的線程的寫鎖下的寫操作,只要寫鎖被釋放數據更新還是可以看到的,所以說,上述這句話“阻塞讀鎖的獲取,那么當前線程無法感知線程T的數據更新” 感覺有些瑕疵
但是需要明確的一點:用鎖降級的前提是讀優先於寫。
比如常見的查詢系統,需要保證數據的隨時可讀,如果當新線程請求讀鎖的時候,當前持有寫鎖的線程需要馬上進行降級,保證所有讀鎖的順利獲取,阻塞后續寫鎖。