溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Java 并發(fā)包中的讀寫鎖及其實現(xiàn)分析

發(fā)布時間:2021-11-09 17:24:13 來源:億速云 閱讀:144 作者:柒染 欄目:編程語言

這期內(nèi)容當中小編將會給大家?guī)碛嘘PJava 并發(fā)包中的讀寫鎖及其實現(xiàn)分析,文章內(nèi)容豐富且以專業(yè)的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。

1. 前言

在Java并發(fā)包中常用的鎖(如:ReentrantLock),基本上都是排他鎖,這些鎖在同一時刻只允許一個線程進行訪問,而讀寫鎖在同一時  刻可以允許多個讀線程訪問,但是在寫線程訪問時,所有的讀線程和其他寫線程均被阻塞。讀寫鎖維護了一對鎖,一個讀鎖和一個寫鎖,通過分離讀鎖和寫鎖,使得  并發(fā)性相比一般的排他鎖有了很大提升。

除了保證寫操作對讀操作的可見性以及并發(fā)性的提升之外,讀寫鎖能夠簡化讀寫交互場景的編程方式。假設在程序中定義一個共享的數(shù)據(jù)結(jié)構(gòu)用作緩存,它大部分時間提供讀服務(例如:查詢和搜索),而寫操作占有的時間很少,但是寫操作完成之后的更新需要對后續(xù)的讀服務可見。

在沒有讀寫鎖支持的(Java 5  之前)時候,如果需要完成上述工作就要使用Java的等待通知機制,就是當寫操作開始時,所有晚于寫操作的讀操作均會進入等待狀態(tài),只有寫操作完成并進行   通知之后,所有等待的讀操作才能繼續(xù)執(zhí)行(寫操作之間依靠synchronized關鍵字進行同步),這樣做的目的是使讀操作都能讀取到正確的數(shù)據(jù),而不   會出現(xiàn)臟讀。改用讀寫鎖實現(xiàn)上述功能,只需要在讀操作時獲取讀鎖,而寫操作時獲取寫鎖即可,當寫鎖被獲取到時,后續(xù)(非當前寫操作線程)的讀寫操作都會被  阻塞,寫鎖釋放之后,所有操作繼續(xù)執(zhí)行,編程方式相對于使用等待通知機制的實現(xiàn)方式而言,變得簡單明了。

一般情況下,讀寫鎖的性能都會比排它鎖要好,因為大多數(shù)場景讀是多于寫的。在讀多于寫的情況下,讀寫鎖能夠提供比排它鎖更好的并發(fā)性和吞吐量。Java并發(fā)包提供讀寫鎖的實現(xiàn)是ReentrantReadWriteLock,它提供的特性如表1所示。

表1. ReentrantReadWriteLock的特性

特性

說明

公平性選擇

支持非公平(默認)和公平的鎖獲取方式,吞吐量還是非公平優(yōu)于公平

重進入

該鎖支持重進入,以讀寫線程為例:讀線程在獲取了讀鎖之后,能夠再次獲取讀鎖。而寫線程在獲取了寫鎖之后能夠再次獲取寫鎖,同時也可以獲取讀鎖

鎖降級

遵循獲取寫鎖、獲取讀鎖再釋放寫鎖的次序,寫鎖能夠降級成為讀鎖

2. 讀寫鎖的接口與示例

ReadWriteLock僅定義了獲取讀鎖和寫鎖的兩個方法,即readLock()和writeLock()方法,而其實現(xiàn)—  ReentrantReadWriteLock,除了接口方法之外,還提供了一些便于外界監(jiān)控其內(nèi)部工作狀態(tài)的方法,這些方法以及描述如表2所示。

表2. ReentrantReadWriteLock展示內(nèi)部工作狀態(tài)的方法

方法名稱

描述

int getReadLockCount()

返回當前讀鎖被獲取的次數(shù)。該次數(shù)不等于獲取讀鎖的線程數(shù),比如:僅一個線程,它連續(xù)獲?。ㄖ剡M入)了n次讀鎖,那么占據(jù)讀鎖的線程數(shù)是1,但該方法返回n

int getReadHoldCount()

返回當前線程獲取讀鎖的次數(shù)。該方法在Java 6 中加入到ReentrantReadWriteLock中,使用ThreadLocal保存當前線程獲取的次數(shù),這也使得Java 6 的實現(xiàn)變得更加復雜

boolean isWriteLocked()

判斷寫鎖是否被獲取

int getWriteHoldCount()

返回當前寫鎖被獲取的次數(shù)

接下來通過一個緩存示例說明讀寫鎖的使用方式,示例代碼如代碼清單1所示。

代碼清單1. Cache.java

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();     }   }   // 清空所有的內(nèi)容   public static final void clear() {     w.lock();     try {       map.clear();     } finally {       w.unlock();     }   } }

上述示例中,Cache組合了一個非線程安全的HashMap作為緩存的實現(xiàn),同時使用讀寫鎖的讀鎖和寫鎖來保證Cache是線程安全的。在讀操作  get(String key)方法中,需要獲取讀鎖,這使得并發(fā)訪問該方法時不會被阻塞。寫操作put(String key, Object  value)和clear()方法,在更新HashMap時必須提前獲取寫鎖,當寫鎖被獲取后,其他線程對于讀鎖和寫鎖的獲取均被阻塞,而只有寫鎖被釋放  之后,其他讀寫操作才能繼續(xù)。Cache使用讀寫鎖提升讀操作并發(fā)性,也保證每次寫操作對所有的讀寫操作的可見性,同時簡化了編程方式。

3. 讀寫鎖的實現(xiàn)分析

接下來將分析ReentrantReadWriteLock的實現(xiàn),主要包括:讀寫狀態(tài)的設計、寫鎖的獲取與釋放、讀鎖的獲取與釋放以及鎖降級(以下沒有特別說明讀寫鎖均可認為是ReentrantReadWriteLock)。

3.1 讀寫狀態(tài)的設計

讀寫鎖同樣依賴自定義同步器來實現(xiàn)同步功能,而讀寫狀態(tài)就是其同步器的同步狀態(tài)。回想ReentrantLock中自定義同步器的實現(xiàn),同步狀態(tài)  表示鎖被一個線程重復獲取的次數(shù),而讀寫鎖的自定義同步器需要在同步狀態(tài)(一個整型變量)上維護多個讀線程和一個寫線程的狀態(tài),使得該狀態(tài)的設計成為讀寫  鎖實現(xiàn)的關鍵。

如果在一個整型變量上維護多種狀態(tài),就一定需要“按位切割使用”這個變量,讀寫鎖是將變量切分成了兩個部分,高16位表示讀,低16位表示寫,劃分方式如圖1所示。

圖1. 讀寫鎖狀態(tài)的劃分方式

Java 并發(fā)包中的讀寫鎖及其實現(xiàn)分析

如圖1所示,當前同步狀態(tài)表示一個線程已經(jīng)獲取了寫鎖,且重進入了兩次,同時也連續(xù)獲取了兩次讀鎖。讀寫鎖是如何迅速的確定讀和寫各自的狀態(tài)呢?  答案是通過位運算。假設當前同步狀態(tài)值為S,寫狀態(tài)等于 S & 0x0000FFFF(將高16位全部抹去),讀狀態(tài)等于 S  >>> 16(無符號補0右移16位)。當寫狀態(tài)增加1時,等于S + 1,當讀狀態(tài)增加1時,等于S + (1 <<  16),也就是S + 0&times;00010000。

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

3.2 寫鎖的獲取與釋放

寫鎖是一個支持重進入的排它鎖。如果當前線程已經(jīng)獲取了寫鎖,則增加寫狀態(tài)。如果當前線程在獲取寫鎖時,讀鎖已經(jīng)被獲取(讀狀態(tài)不為0)或者該線程不是已經(jīng)獲取寫鎖的線程,則當前線程進入等待狀態(tài),獲取寫鎖的代碼如代碼清單2所示。

代碼清單2. ReentrantReadWriteLock的tryAcquire方法

protected final boolean tryAcquire(int acquires) {   Thread current = Thread.currentThread();   int c = getState();   int w = exclusiveCount(c);   if (c != 0) {     // 存在讀鎖或者當前獲取線程不是已經(jīng)獲取寫鎖的線程     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; }

該方法除了重入條件(當前線程為獲取了寫鎖的線程)之外,增加了一個讀鎖是否存在的判斷。如果存在讀鎖,則寫鎖不能被獲取,原因在于:讀寫鎖要確保   寫鎖的操作對讀鎖可見,如果允許讀鎖在已被獲取的情況下對寫鎖的獲取,那么正在運行的其他讀線程就無法感知到當前寫線程的操作。因此只有等待其他讀線程都  釋放了讀鎖,寫鎖才能被當前線程所獲取,而寫鎖一旦被獲取,則其他讀寫線程的后續(xù)訪問均被阻塞。

寫鎖的釋放與ReentrantLock的釋放過程基本類似,每次釋放均減少寫狀態(tài),當寫狀態(tài)為0時表示寫鎖已被釋放,從而等待的讀寫線程能夠繼續(xù)訪問讀寫鎖,同時前次寫線程的修改對后續(xù)讀寫線程可見。

3.3 讀鎖的獲取與釋放

讀鎖是一個支持重進入的共享鎖,它能夠被多個線程同時獲取,在沒有其他寫線程訪問(或者寫狀態(tài)為0)時,讀鎖總會成功的被獲取,而所做的也只是  (線程安全的)增加讀狀態(tài)。如果當前線程已經(jīng)獲取了讀鎖,則增加讀狀態(tài)。如果當前線程在獲取讀鎖時,寫鎖已被其他線程獲取,則進入等待狀態(tài)。獲取讀鎖的實  現(xiàn)從Java 5到Java  6變得復雜許多,主要原因是新增了一些功能,比如:getReadHoldCount()方法,返回當前線程獲取讀鎖的次數(shù)。讀狀態(tài)是所有線程獲取讀鎖次   數(shù)的總和,而每個線程各自獲取讀鎖的次數(shù)只能選擇保存在ThreadLocal中,由線程自身維護,這使獲取讀鎖的實現(xiàn)變得復雜。因此,這里將獲取讀鎖的  代碼做了刪減,保留必要的部分,代碼如代碼清單3所示。

代碼清單3. ReentrantReadWriteLock的tryAcquireShared方法

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;   } }

在tryAcquireShared(int unused)方法中,如果其他線程已經(jīng)獲取了寫鎖,則當前線程獲取讀鎖失敗,進入等待狀態(tài)。如果當前線程獲取了寫鎖或者寫鎖未被獲取,則當前線程(線程安全,依靠CAS保證)增加讀狀態(tài),成功獲取讀鎖。

讀鎖的每次釋放均(線程安全的,可能有多個讀線程同時釋放讀鎖)減少讀狀態(tài),減少的值是(1 << 16)。

3.4 鎖降級

鎖降級指的是寫鎖降級成為讀鎖。如果當前線程擁有寫鎖,然后將其釋放,***再獲取讀鎖,這種分段完成的過程不能稱之為鎖降級。鎖降級是指把持住(當前擁有的)寫鎖,再獲取到讀鎖,隨后釋放(先前擁有的)寫鎖的過程。

接下來看一個鎖降級的示例:因為數(shù)據(jù)不常變化,所以多個線程可以并發(fā)的進行數(shù)據(jù)處理,當數(shù)據(jù)變更后,當前線程如果感知到數(shù)據(jù)變化,則進行數(shù)據(jù)的準備工作,同時其他處理線程被阻塞,直到當前線程完成數(shù)據(jù)的準備工作,示例代碼如代碼清單4所示。

代碼清單4. processData方法

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

上述示例中,當數(shù)據(jù)發(fā)生變更后,update變量(布爾類型且Volatile修飾)被設置為false,此時所有訪問processData() 方法的線程都能夠感知到變化,但只有一個線程能夠獲取到寫鎖,而其他線程會被阻塞在讀鎖和寫鎖的lock()方法上。當前程獲取寫鎖完成數(shù)據(jù)準備之后,再 獲取讀鎖,隨后釋放寫鎖,完成鎖降級。

鎖降級中讀鎖的獲取是否必要呢?答案是必要的。主要原因是保證數(shù)據(jù)的可見性,如果當前線程不獲取讀鎖而是直接釋放寫鎖,假設此刻另一個線程(記作  線程T)獲取了寫鎖并修改了數(shù)據(jù),則當前線程無法感知線程T的數(shù)據(jù)更新。如果當前線程獲取讀鎖,即遵循鎖降級的步驟,則線程T將會被阻塞,直到當前線程使  用數(shù)據(jù)并釋放讀鎖之后,線程T才能獲取寫鎖進行數(shù)據(jù)更新。

RentrantReadWriteLock不支持鎖升級(把持讀鎖、獲取寫鎖,***釋放讀鎖的過程)。原因也是保證數(shù)據(jù)可見性,如果讀鎖已被多個線程獲取,其中任意線程成功獲取了寫鎖并更新了數(shù)據(jù),則其更新對其他獲取到讀鎖的線程不可見。

上述就是小編為大家分享的Java 并發(fā)包中的讀寫鎖及其實現(xiàn)分析了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注億速云行業(yè)資訊頻道。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

AI