Java Concurrency #6: JUC — 讓鎖更有效率的讀寫鎖ReadWriteLock
使用JUC的ReadWriteLock API簡單實踐,讀寫鎖功能
為何需要ReadWriteLock
在前面的系列文,提及的lock與設計的lock目標都是透過互斥保護相同的操作,但是因為互斥一次只能讓單個Thread執行,所以會讓Concurrency的優勢降低,所以在設計Concurrency程式時非不得已在鎖住資源是非常關鍵的設計思考基礎。在Java中提供非常多方式讓開發者可以不鎖住資源,又可以確保不會有可見性與原子性問題,本文主要介紹讀寫鎖ReadWriteLock。
讀寫鎖和基本鎖有何差別?
先仔細思考,每次上鎖都是很大部份都是保護資料,並且確保資料一次只會被一個Thread修改,可是若是單純讀的情況,再沒有修改過資料狀況下是不是就不用排隊拿鎖了,所以讀寫鎖最基本功能就是,寫的時候上鎖,讀的時候如果資料沒修改過那就直接讀,那如果有修改呢就會有以下情形
如果在讀資料的同時,有Thread在背景馬上寫入新的資料,那讀的資料就會不一致,例如: A使用者買限量票的時 系統當下抓出來的剩餘票數還有剩餘一張,但其實在同一時間有令一個B使用者將此票買走了,那系統在A使用者的購票請求就會出現Bug(因為實際已經沒有票可以販售)
包含要處理以上的邏輯,那讀寫鎖就會有以下的規則
- 可以一起讀
- 不可一起寫
- 讀要等寫
言簡意賅,就是寫寫互斥,讀寫互斥,讀讀不互斥,寫大於讀。
如果是單純使用Synchronized設計會有點麻煩(需要自定義每個狀態,還需要確保狀態的可見性與原子性),使用Reentrance的condiction會讓設計簡單點,其實JUC已經提供ReadWriteLock,並且設計好上述的規則,開發者只要初始化Lock就可以使用。
ReadWriteLock 範例
這是一個FileEditor範例,可以讓多個執行緒使用
在初始化RWLock之後,在分別從中取出W and R Lock,而兩個Lock的互斥規則如本篇文章前言。JUC的讀寫鎖就這簡單可以使用,不需要在自己handle複雜的邏輯。
進階 — 讀寫 升級/降級
根據前言提到的讀寫鎖規則,可以知道寫鎖優先權大於讀,所以讀寫的升級與降級也代表:
- 讀鎖升級成寫鎖
- 寫鎖降級成讀鎖。
在JUC的ReadWriteLock中,是不允許鎖的升級只允許降級(這裡提到的升級降級都是在同一個Thread的邏輯),先來看看範例程式 (快取的實踐):
此段範例碼的功能是在寫入資料庫前的快取機制,寫入時會更新isChanged 標記已經變動,在讀取時如果資料沒被更動則會直接回傳,若是更動則會再次讀取DB拿出資料。程式碼重點如下:
- 讀寫升降級的邏輯集中在get方法
- ReadWriteLock不支持鎖升級:所以在判斷已更動要重新拿取資料須先解放rLock,如果直接wLock.lock的話會永久blocking
- wLock.lock之後會在判斷一次isChanged:因為這是Concurrency Program,或許在要對DB操作前已經有其他Thread刷新Cache了,而在解放wLock前要再次鎖住rLock,預防釋放wLock之後資料右再次被更改,在程式的最後finally在釋放rLock。
結論
JUC提供了簡單使用的ReadWriteLock,開發者可以不用自己在Synchronized實踐,可以減去許多確保值得原子性和可見性操作,文章中包含了簡單的使用(FileEditor)與讀寫降級的實踐(DB的Cache機制),ReadWriteLock只提供了寫降級成讀鎖,不提供讀升級成寫鎖(如果不小心使用了不會噴例外,但是會無限Blocking),切記。