1. 前言html
在Java併發包中經常使用的鎖(如:ReentrantLock),基本上都是排他鎖,這些鎖在同一時刻只容許一個線程進行訪問,而讀寫鎖在同一時 刻能夠容許多個讀線程訪問,可是在寫線程訪問時,全部的讀線程和其餘寫線程均被阻塞。讀寫鎖維護了一對鎖,一個讀鎖和一個寫鎖,經過分離讀鎖和寫鎖,使得 併發性相比通常的排他鎖有了很大提高。java
除了保證寫操做對讀操做的可見性以及併發性的提高以外,讀寫鎖可以簡化讀寫交互場景的編程方式。假設在程序中定義一個共享的數據結構用做緩存,它大部分時間提供讀服務(例如:查詢和搜索),而寫操做佔有的時間不多,可是寫操做完成以後的更新須要對後續的讀服務可見。編程
在沒有讀寫鎖支持的(Java 5 以前)時候,若是須要完成上述工做就要使用Java的等待通知機制,就是當寫操做開始時,全部晚於寫操做的讀操做均會進入等待狀態,只有寫操做完成並進行 通知以後,全部等待的讀操做才能繼續執行(寫操做之間依靠synchronized關鍵字進行同步),這樣作的目的是使讀操做都能讀取到正確的數據,而不 會出現髒讀。改用讀寫鎖實現上述功能,只須要在讀操做時獲取讀鎖,而寫操做時獲取寫鎖便可,當寫鎖被獲取到時,後續(非當前寫操做線程)的讀寫操做都會被 阻塞,寫鎖釋放以後,全部操做繼續執行,編程方式相對於使用等待通知機制的實現方式而言,變得簡單明瞭。緩存
通常狀況下,讀寫鎖的性能都會比排它鎖要好,由於大多數場景讀是多於寫的。在讀多於寫的狀況下,讀寫鎖可以提供比排它鎖更好的併發性和吞吐量。Java併發包提供讀寫鎖的實現是ReentrantReadWriteLock,它提供的特性如表1所示。安全
表1. ReentrantReadWriteLock的特性數據結構
特性併發 |
說明性能 |
公平性選擇ui |
支持非公平(默認)和公平的鎖獲取方式,吞吐量仍是非公平優於公平spa |
重進入 |
該鎖支持重進入,以讀寫線程爲例:讀線程在獲取了讀鎖以後,可以再次獲取讀鎖。而寫線程在獲取了寫鎖以後可以再次獲取寫鎖,同時也能夠獲取讀鎖 |
鎖降級 |
遵循獲取寫鎖、獲取讀鎖再釋放寫鎖的次序,寫鎖可以降級成爲讀鎖 |
2. 讀寫鎖的接口與示例
ReadWriteLock僅定義了獲取讀鎖和寫鎖的兩個方法,即readLock()和writeLock()方法,而其實現— ReentrantReadWriteLock,除了接口方法以外,還提供了一些便於外界監控其內部工做狀態的方法,這些方法以及描述如表2所示。
表2. ReentrantReadWriteLock展現內部工做狀態的方法
方法名稱 |
描述 |
int getReadLockCount() |
返回當前讀鎖被獲取的次數。該次數不等於獲取讀鎖的線程數,好比:僅一個線程,它連續獲取(重進入)了n次讀鎖,那麼佔據讀鎖的線程數是1,但該方法返回n |
int getReadHoldCount() |
返回當前線程獲取讀鎖的次數。該方法在Java 6 中加入到ReentrantReadWriteLock中,使用ThreadLocal保存當前線程獲取的次數,這也使得Java 6 的實現變得更加複雜 |
boolean isWriteLocked() |
判斷寫鎖是否被獲取 |
int getWriteHoldCount() |
返回當前寫鎖被獲取的次數 |
接下來經過一個緩存示例說明讀寫鎖的使用方式,示例代碼如代碼清單1所示。
代碼清單1. Cache.java
上述示例中,Cache組合了一個非線程安全的HashMap做爲緩存的實現,同時使用讀寫鎖的讀鎖和寫鎖來保證Cache是線程安全的。在讀操做 get(String key)方法中,須要獲取讀鎖,這使得併發訪問該方法時不會被阻塞。寫操做put(String key, Object value)和clear()方法,在更新HashMap時必須提早獲取寫鎖,當寫鎖被獲取後,其餘線程對於讀鎖和寫鎖的獲取均被阻塞,而只有寫鎖被釋放 以後,其餘讀寫操做才能繼續。Cache使用讀寫鎖提高讀操做併發性,也保證每次寫操做對全部的讀寫操做的可見性,同時簡化了編程方式。
3. 讀寫鎖的實現分析
接下來將分析ReentrantReadWriteLock的實現,主要包括:讀寫狀態的設計、寫鎖的獲取與釋放、讀鎖的獲取與釋放以及鎖降級(如下沒有特別說明讀寫鎖都可認爲是ReentrantReadWriteLock)。
3.1 讀寫狀態的設計
讀寫鎖一樣依賴自定義同步器來實現同步功能,而讀寫狀態就是其同步器的同步狀態。回想ReentrantLock中自定義同步器的實現,同步狀態 表示鎖被一個線程重複獲取的次數,而讀寫鎖的自定義同步器須要在同步狀態(一個整型變量)上維護多個讀線程和一個寫線程的狀態,使得該狀態的設計成爲讀寫 鎖實現的關鍵。
若是在一個整型變量上維護多種狀態,就必定須要「按位切割使用」這個變量,讀寫鎖是將變量切分紅了兩個部分,高16位表示讀,低16位表示寫,劃分方式如圖1所示。
圖1. 讀寫鎖狀態的劃分方式
如圖1所示,當前同步狀態表示一個線程已經獲取了寫鎖,且重進入了兩次,同時也連續獲取了兩次讀鎖。讀寫鎖是如何迅速的肯定讀和寫各自的狀態呢? 答案是經過位運算。假設當前同步狀態值爲S,寫狀態等於 S & 0x0000FFFF(將高16位所有抹去),讀狀態等於 S >>> 16(無符號補0右移16位)。當寫狀態增長1時,等於S + 1,當讀狀態增長1時,等於S + (1 << 16),也就是S + 0×00010000。
根據狀態的劃分能得出一個推論:S不等於0時,當寫狀態(S & 0x0000FFFF)等於0時,則讀狀態(S >>> 16)大於0,即讀鎖已被獲取。
3.2 寫鎖的獲取與釋放
寫鎖是一個支持重進入的排它鎖。若是當前線程已經獲取了寫鎖,則增長寫狀態。若是當前線程在獲取寫鎖時,讀鎖已經被獲取(讀狀態不爲0)或者該線程不是已經獲取寫鎖的線程,則當前線程進入等待狀態,獲取寫鎖的代碼如代碼清單2所示。
代碼清單2. ReentrantReadWriteLock的tryAcquire方法
該方法除了重入條件(當前線程爲獲取了寫鎖的線程)以外,增長了一個讀鎖是否存在的判斷。若是存在讀鎖,則寫鎖不能被獲取,緣由在於:讀寫鎖要確保 寫鎖的操做對讀鎖可見,若是容許讀鎖在已被獲取的狀況下對寫鎖的獲取,那麼正在運行的其餘讀線程就沒法感知到當前寫線程的操做。所以只有等待其餘讀線程都 釋放了讀鎖,寫鎖才能被當前線程所獲取,而寫鎖一旦被獲取,則其餘讀寫線程的後續訪問均被阻塞。
寫鎖的釋放與ReentrantLock的釋放過程基本相似,每次釋放均減小寫狀態,當寫狀態爲0時表示寫鎖已被釋放,從而等待的讀寫線程可以繼續訪問讀寫鎖,同時前次寫線程的修改對後續讀寫線程可見。
3.3 讀鎖的獲取與釋放
讀鎖是一個支持重進入的共享鎖,它可以被多個線程同時獲取,在沒有其餘寫線程訪問(或者寫狀態爲0)時,讀鎖總會成功的被獲取,而所作的也只是 (線程安全的)增長讀狀態。若是當前線程已經獲取了讀鎖,則增長讀狀態。若是當前線程在獲取讀鎖時,寫鎖已被其餘線程獲取,則進入等待狀態。獲取讀鎖的實 現從Java 5到Java 6變得複雜許多,主要緣由是新增了一些功能,好比:getReadHoldCount()方法,返回當前線程獲取讀鎖的次數。讀狀態是全部線程獲取讀鎖次 數的總和,而每一個線程各自獲取讀鎖的次數只能選擇保存在ThreadLocal中,由線程自身維護,這使獲取讀鎖的實現變得複雜。所以,這裏將獲取讀鎖的 代碼作了刪減,保留必要的部分,代碼如代碼清單3所示。
代碼清單3. ReentrantReadWriteLock的tryAcquireShared方法
在tryAcquireShared(int unused)方法中,若是其餘線程已經獲取了寫鎖,則當前線程獲取讀鎖失敗,進入等待狀態。若是當前線程獲取了寫鎖或者寫鎖未被獲取,則當前線程(線程安全,依靠CAS保證)增長讀狀態,成功獲取讀鎖。
讀鎖的每次釋放均(線程安全的,可能有多個讀線程同時釋放讀鎖)減小讀狀態,減小的值是(1 << 16)。
3.4 鎖降級
鎖降級指的是寫鎖降級成爲讀鎖。若是當前線程擁有寫鎖,而後將其釋放,最後再獲取讀鎖,這種分段完成的過程不能稱之爲鎖降級。鎖降級是指把持住(當前擁有的)寫鎖,再獲取到讀鎖,隨後釋放(先前擁有的)寫鎖的過程。
接下來看一個鎖降級的示例:由於數據不常變化,因此多個線程能夠併發的進行數據處理,當數據變動後,當前線程若是感知到數據變化,則進行數據的準備工做,同時其餘處理線程被阻塞,直到當前線程完成數據的準備工做,示例代碼如代碼清單4所示。
代碼清單4. processData方法
上述示例中,當數據發生變動後,update變量(布爾類型且Volatile修飾)被設置爲false,此時全部訪問processData() 方法的線程都可以感知到變化,但只有一個線程可以獲取到寫鎖,而其餘線程會被阻塞在讀鎖和寫鎖的lock()方法上。當前程獲取寫鎖完成數據準備以後,再 獲取讀鎖,隨後釋放寫鎖,完成鎖降級。
鎖降級中讀鎖的獲取是否必要呢?答案是必要的。主要緣由是保證數據的可見性,若是當前線程不獲取讀鎖而是直接釋放寫鎖,假設此刻另外一個線程(記做 線程T)獲取了寫鎖並修改了數據,則當前線程沒法感知線程T的數據更新。若是當前線程獲取讀鎖,即遵循鎖降級的步驟,則線程T將會被阻塞,直到當前線程使 用數據並釋放讀鎖以後,線程T才能獲取寫鎖進行數據更新。
RentrantReadWriteLock不支持鎖升級(把持讀鎖、獲取寫鎖,最後釋放讀鎖的過程)。緣由也是保證數據可見性,若是讀鎖已被多個線程獲取,其中任意線程成功獲取了寫鎖並更新了數據,則其更新對其餘獲取到讀鎖的線程不可見。