1. 程式人生 > >讀寫鎖ReentrantReadWriteLock

讀寫鎖ReentrantReadWriteLock

       讀寫鎖維護一對鎖,一個讀鎖和一個寫鎖。讀寫鎖在同一時刻可以允許多個讀執行緒訪問,但是在寫執行緒訪問時,所有的讀執行緒和其他寫執行緒均被阻塞。一般情況下,讀寫鎖的效能都會比排它鎖好,因為大多數場景讀是多於寫的。特性如下:

  • 公平性選擇
  • 重進入
  • 鎖降級

1.讀寫鎖的介面與例項

        ReadWriteLock僅定義了獲取讀鎖和寫鎖的兩個方法,即readLock()方法和writeLock()方法,而其實現——ReentrantReadWriteLock除了介面方法之外,還提供了一些便於外界監控其內部工作狀態的方法。

通過一個快取示例說明讀寫鎖的使用方式

public class Cache {
		static Map<String, Object> map = new HashMap<String, Object>();
		static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
		static Lock r = rwl.readLock();
		static Lock w = rwl.writeLock();

		// 獲取一個key對應的value
		public static final Object get(String key) {
			r.lock();
			try {
				return map.get(key);
			} finally {
				r.unlock();
			}
		}

		// 設定key對應的value,並返回舊的value
		public static final Object put(String key, Object value) {
			w.lock();
			try {
				return map.put(key, value);
			} finally {
				w.unlock();
			}
		}

		// 清空所有的內容
		public static final void clear() {
			w.lock();
			try {
				map.clear();
			} finally {
				w.unlock();
			}
		}
	}

2.讀寫鎖的實現

主要包括:讀寫狀態的設計、寫鎖的獲取與釋放、讀鎖的獲取與釋放以及鎖降級。

2.1讀寫狀態的設計

        讀寫鎖同樣依賴自定義同步器來實現同步功能,,而讀寫狀態就是其同步器的同步狀態(state變數)。

        一個整型變數上維護多種狀態,就一定需要“按位切割使用”這個變數,讀寫鎖將變數切分成了兩個部分,高16位表示讀,低16位表示寫。

        當前同步狀態表示一個執行緒已經獲取了寫鎖,且重進入了兩次,同時也連續獲取了兩次讀鎖。

        假設當前同步狀態值為S,寫狀態等於S&0x0000FFFF(將高16位全部抹去),讀狀態等於S>>>16(無符號補0右移16位)。當寫狀態增加1時,等於S+1,當讀狀態增加1時,等於S+(1<<16),也就是S+0x00010000。

        根據狀態的劃分能得出一個推論:S不等於0時,當寫狀態(S&0x0000FFFF)等於0時,則讀狀態(S>>>16)大於0,即讀鎖已被獲取。

2.2寫鎖的獲取與釋放

寫鎖是一個支援重進入的排它鎖,如果當前執行緒在獲取寫鎖時,讀鎖已經被獲取(讀狀態不為0)或者該執行緒不是已經獲取寫鎖的執行緒,則當前執行緒進入等待狀態。

ReentrantReadWriteLock的tryAcquire方法

protected final boolean tryAcquire(int acquires) {
		Thread current = Thread.currentThread();
		int c = getState();
		int w = exclusiveCount(c);
		if (c != 0) {
			// 存在讀鎖或者當前獲取執行緒不是已經獲取寫鎖的執行緒
			if (w == 0 || current != getExclusiveOwnerThread())
				return false;
			if (w + exclusiveCount(acquires) > MAX_COUNT)
				throw new Error("Maximum lock count exceeded");
			setState(c + acquires);
			return true;
		}
		if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) {
			return false;
		}
		setExclusiveOwnerThread(current);
		return true;
	}

        如果存在讀鎖,則寫鎖不能被獲取,原因在於:讀寫鎖要確保寫鎖的操作對讀鎖可見。

        寫鎖的釋放與ReentrantLock的釋放過程基本類似,每次釋放均減少寫狀態,當寫狀態為0時表示寫鎖已被釋放,從而等待的讀寫執行緒能夠繼續訪問讀寫鎖,同時前次寫執行緒的修改對後續讀寫執行緒可見。

2.3讀鎖的獲取釋放

protected final int tryAcquireShared(int unused) {
		for (;;) {
			int c = getState();
			int nextc = c + (1 << 16);
			if (nextc < c)
				throw new Error("Maximum lock count exceeded");
			if (exclusiveCount(c) != 0 && owner != Thread.currentThread())
				return -1;
			if (compareAndSetState(c, nextc))
				return 1;
		}
	}

        如果其他執行緒已經獲取了寫鎖,則當前執行緒獲取讀鎖失敗,進入等待狀態。如果當前執行緒獲取了寫鎖或者寫鎖未被獲取,則當前執行緒(執行緒安全,
依靠CAS保證)增加讀狀態,成功獲取讀鎖。

    讀鎖的每次釋放(執行緒安全的,可能有多個讀執行緒同時釋放讀鎖)均減少讀狀態,減少的值是(1<<16)。

2.4鎖降級

        鎖降級指的是寫鎖降級成為讀鎖。鎖降級是指把持住(當前擁有的)寫鎖,再獲取到讀鎖,隨後釋放(先前擁有的)寫鎖的過程。

public void processData() {
		readLock.lock();
		if (!update) {
			// 必須先釋放讀鎖
			readLock.unlock();
			// 鎖降級從寫鎖獲取到開始
			writeLock.lock();
			try {
				if (!update) {
					// 準備資料的流程(略)
					update = true;
				}
				readLock.lock();
			} finally {
				writeLock.unlock();
			}
			// 鎖降級完成,寫鎖降級為讀鎖
		}
		try {
			// 使用資料的流程(略)
		} finally {
			readLock.unlock();
		}
	}

        鎖降級中讀鎖的獲取是否必要呢?答案是必要的。主要是為了保證資料的可見性,如果當前執行緒不獲取讀鎖而是直接釋放寫鎖,假設此刻另一個執行緒(記作執行緒T)獲取了寫鎖並修改了資料,那麼當前執行緒無法感知執行緒T的資料更新。如果當前執行緒獲取讀鎖,即遵循鎖降級的步驟,則執行緒T將會被阻塞,直到當前執行緒使用資料並釋放讀鎖之後,執行緒T才能獲取寫鎖進行資料更新。

     RentrantReadWriteLock不支援鎖升級, 鎖升級無法保