簡體   English   中英

Java ReentrantReadWriteLocks - 如何安全地獲取寫鎖?

[英]Java ReentrantReadWriteLocks - how to safely acquire write lock?

我現在在我的代碼中使用ReentrantReadWriteLock來同步對樹狀結構的訪問。 這個結構很大,可以同時被多個線程讀取,偶爾會修改它的小部分——所以它似乎很適合讀寫習慣用法。 我知道對於這個特定的類,不能將讀鎖提升為寫鎖,因此根據 Javadocs 必須在獲得寫鎖之前釋放讀鎖。 我以前在不可重入的上下文中成功地使用了這種模式。

然而,我發現我無法在不永遠阻塞的情況下可靠地獲取寫鎖。 由於讀鎖是可重入的並且我實際上是這樣使用它的,簡單的代碼

lock.getReadLock().unlock();
lock.getWriteLock().lock()

如果我以可重入的方式獲取了讀鎖,則可以阻止。 每次調用 unlock 只會減少保持計數,並且只有當保持計數為零時才會真正釋放鎖。

編輯以澄清這一點,因為我認為我最初沒有很好地解釋它 - 我知道此類中沒有內置鎖升級,並且我必須簡單地釋放讀鎖並獲取寫鎖。 我的問題是/是無論其他線程在做什么,調用getReadLock().unlock()可能實際上不會釋放線程對鎖的持有,如果它可重入獲取它,在這種情況下調用getWriteLock().lock()將永遠阻塞,因為該線程仍然持有讀鎖並因此阻塞自身。

例如,即使在沒有其他線程訪問鎖的情況下單線程運行時,此代碼片段也永遠不會到達 println 語句:

final ReadWriteLock lock = new ReentrantReadWriteLock();
lock.getReadLock().lock();

// In real code we would go call other methods that end up calling back and
// thus locking again
lock.getReadLock().lock();

// Now we do some stuff and realise we need to write so try to escalate the
// lock as per the Javadocs and the above description
lock.getReadLock().unlock(); // Does not actually release the lock
lock.getWriteLock().lock();  // Blocks as some thread (this one!) holds read lock

System.out.println("Will never get here");

所以我問,有沒有一個很好的成語來處理這種情況? 具體來說,當一個持有讀鎖(可能是可重入的)的線程發現它需要做一些寫操作,因此想要“掛起”自己的讀鎖以獲取寫鎖(在其他線程上按要求阻塞)釋放他們對讀鎖的持有),然后在相同的狀態下“拿起”它對讀鎖的持有?

由於這個 ReadWriteLock 實現是專門設計為可重入的,當可以重入獲取鎖時,肯定有一些明智的方法可以將讀鎖提升為寫鎖嗎? 這是關鍵部分,這意味着幼稚的方法不起作用。

我在這方面取得了一些進展。 通過將鎖變量顯式聲明為ReentrantReadWriteLock而不是簡單的ReadWriteLock (不太理想,但在這種情況下可能是必要的邪惡),我可以調用getReadHoldCount()方法。 這讓我可以獲得當前線程的保留次數,因此我可以多次釋放讀鎖(然后重新獲取相同的數量)。 所以這是有效的,如快速和骯臟的測試所示:

final int holdCount = lock.getReadHoldCount();
for (int i = 0; i < holdCount; i++) {
   lock.readLock().unlock();
}
lock.writeLock().lock();
try {
   // Perform modifications
} finally {
   // Downgrade by reacquiring read lock before releasing write lock
   for (int i = 0; i < holdCount; i++) {
      lock.readLock().lock();
   }
   lock.writeLock().unlock();
}

不過,這會是我能做的最好的嗎? 感覺不是很優雅,我仍然希望有一種方法可以以不那么“手動”的方式來處理這個問題。

這是一個老問題,但這里既有問題的解決方案,也有一些背景信息。

正如其他人指出的那樣,經典的讀寫鎖(如 JDK ReentrantReadWriteLock )本質上不支持將讀鎖升級為寫鎖,因為這樣做容易發生死鎖。

如果您需要在不首先釋放讀鎖的情況下安全地獲取寫鎖,則有一個更好的選擇:改為查看讀-寫-更新鎖。

我寫了一個ReentrantReadWrite_Update_Lock,這下Apache 2.0許可發布為開放源代碼在這里 我還在 JSR166 concurrency-interest 郵件列表 中發布了該方法的詳細信息,並且該方法在該列表中成員的反復審查中幸免於難。

該方法非常簡單,正如我在並發興趣中提到的,這個想法並不是全新的,因為它至少在 2000 年就在 Linux 內核郵件列表中討論過。此外,.Net 平台的ReaderWriterLockSlim支持鎖升級還有。 直到現在,這個概念才有效地在 Java (AFAICT) 上實現。

這個想法是在鎖和鎖之外提供一個更新鎖。 更新鎖是介於讀鎖和寫鎖之間的一種中間鎖。 與寫鎖一樣,一次只有一個線程可以獲取更新鎖。 但就像讀鎖一樣,它允許對持有它的線程進行讀訪問,同時也允許對其他持有常規讀鎖的線程進行讀訪問。 關鍵特性是更新鎖可以從只讀狀態升級到寫鎖,並且不容易死鎖,因為一次只有一個線程可以持有更新鎖並處於升級狀態。

這支持鎖升級,而且在具有先讀后寫訪問模式的應用程序中,它比傳統的讀寫鎖更有效,因為它會在更短的時間內阻止讀取線程。

網站上提供了示例用法。 該庫具有 100% 的測試覆蓋率並且位於 Maven 中心。

你想做的事情應該是可能的。 問題是Java沒有提供可以將讀鎖升級為寫鎖的實現。 具體來說,javadoc ReentrantReadWriteLock 說它不允許從讀鎖升級到寫鎖。

無論如何,Jakob Jenkov 描述了如何實現它。 有關詳細信息,請參閱http://tutorials.jenkov.com/java-concurrency/read-write-locks.html#upgrade

為什么需要將讀鎖升級為寫鎖

從讀鎖升級到寫鎖是有效的(盡管其他答案中的斷言相反)。 可能會發生死鎖,因此實現的一部分是識別死鎖並通過在線程中拋出異常來打破死鎖來打破死鎖的代碼。 這意味着作為事務的一部分,您必須處理 DeadlockException,例如,通過重新進行工作。 一個典型的模式是:

boolean repeat;
do {
  repeat = false;
  try {
   readSomeStuff();
   writeSomeStuff();
   maybeReadSomeMoreStuff();
  } catch (DeadlockException) {
   repeat = true;
  }
} while (repeat);

如果沒有這種能力,實現一個可串行化的事務,一致地讀取一堆數據,然后根據讀取的內容寫入一些東西的唯一方法是在開始之前預測寫入是必要的,因此獲得對所有數據的 WRITE 鎖在寫需要寫的東西之前先讀。 這是 Oracle 使用的 KLUDGE (SELECT FOR UPDATE ...)。 此外,它實際上降低了並發性,因為在事務運行時沒有其他人可以讀取或寫入任何數據!

特別是在獲得寫鎖之前釋放讀鎖會產生不一致的結果。 考慮:

int x = someMethod();
y.writeLock().lock();
y.setValue(x);
y.writeLock().unlock();

您必須知道 someMethod() 或它調用的任何方法是否在 y 上創建了可重入讀鎖! 假設你知道它確實如此。 然后如果你先釋放讀鎖:

int x = someMethod();
y.readLock().unlock();
// problem here!
y.writeLock().lock();
y.setValue(x);
y.writeLock().unlock();

另一個線程可能會在你釋放它的讀鎖之后和你獲得它的寫鎖之前改變 y。 所以 y 的值將不等於 x。

測試代碼:將讀鎖升級為寫鎖塊:

import java.util.*;
import java.util.concurrent.locks.*;

public class UpgradeTest {

    public static void main(String[] args) 
    {   
        System.out.println("read to write test");
        ReadWriteLock lock = new ReentrantReadWriteLock();

        lock.readLock().lock(); // get our own read lock
        lock.writeLock().lock(); // upgrade to write lock
        System.out.println("passed");
    }

}

使用 Java 1.6 輸出:

read to write test
<blocks indefinitely>

您嘗試做的事情根本不可能以這種方式進行。

您不能擁有可以從讀升級到寫而不會出現問題的讀/寫鎖。 示例:

void test() {
    lock.readLock().lock();
    ...
    if ( ... ) {
        lock.writeLock.lock();
        ...
        lock.writeLock.unlock();
    }
    lock.readLock().unlock();
}

現在假設有兩個線程將進入該函數。 (並且您假設並發,對嗎?否則您一開始就不會關心鎖......)

假設兩個線程將開始在同一時間和運行一樣快 這意味着,兩者都會獲得一個讀鎖,這是完全合法的。 但是,然后兩者最終都會嘗試獲取寫鎖,而它們中的任何一個都不會獲得:相應的其他線程持有讀鎖!

根據定義,允許將讀鎖升級為寫鎖的鎖容易發生死鎖。 抱歉,您需要修改您的方法。

Java 8 現在有一個帶有tryConvertToWriteLock(long) API 的java.util.concurrent.locks.StampedLock

更多信息請訪問http://www.javaspecialists.eu/archive/Issue215.html

您正在尋找的是鎖升級,並且使用標准 java.concurrent ReentrantReadWriteLock 是不可能的(至少不是原子的)。 最好的方法是解鎖/鎖定,然后檢查是否沒有人進行修改。

您正在嘗試做的事情,強制所有讀取鎖定都不是一個好主意。 讀鎖是有原因的,你不應該寫。 :)

編輯:
正如 Ran Biron 指出的那樣,如果您的問題是飢餓(讀取鎖一直在設置和釋放,永遠不會降為零),您可以嘗試使用公平排隊。 但你的問題聽起來不像這是你的問題?

編輯2:
我現在看到您的問題,您實際上已經在堆棧上獲得了多個讀鎖,並且您想將它們轉換為寫鎖(升級)。 這實際上對於 JDK 實現是不可能的,因為它不跟蹤讀鎖的所有者。 可能還有其他人持有您看不到的讀鎖,並且它不知道有多少讀鎖屬於您的線程,更不用說您當前的調用堆棧(即您的循環正在殺死所有讀鎖,不只是你自己的,所以你的寫鎖不會等待任何並發讀者完成,你最終會弄得一團糟)

我實際上遇到了類似的問題,我最終編寫了自己的鎖來跟蹤誰擁有讀鎖並將它們升級為寫鎖。 雖然這也是一種 Copy-on-Write 類型的讀/寫鎖(允許一個寫者與讀者同行),所以它仍然有點不同。

像這樣的東西怎么辦?

class CachedData
{
    Object data;
    volatile boolean cacheValid;

    private class MyRWLock
    {
        private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
        public synchronized void getReadLock()         { rwl.readLock().lock(); }
        public synchronized void upgradeToWriteLock()  { rwl.readLock().unlock();  rwl.writeLock().lock(); }
        public synchronized void downgradeToReadLock() { rwl.writeLock().unlock(); rwl.readLock().lock();  }
        public synchronized void dropReadLock()        { rwl.readLock().unlock(); }
    }
    private MyRWLock myRWLock = new MyRWLock();

    void processCachedData()
    {
        myRWLock.getReadLock();
        try
        {
            if (!cacheValid)
            {
                myRWLock.upgradeToWriteLock();
                try
                {
                    // Recheck state because another thread might have acquired write lock and changed state before we did.
                    if (!cacheValid)
                    {
                        data = ...
                        cacheValid = true;
                    }
                }
                finally
                {
                    myRWLock.downgradeToReadLock();
                }
            }
            use(data);
        }
        finally
        {
            myRWLock.dropReadLock();
        }
    }
}

OP:只要您進入鎖就解鎖多少次,就這么簡單:

boolean needWrite = false;
readLock.lock()
try{
  needWrite = checkState();
}finally{
  readLock().unlock()
}

//the state is free to change right here, but not likely
//see who has handled it under the write lock, if need be
if (needWrite){
  writeLock().lock();
  try{
    if (checkState()){//check again under the exclusive write lock
   //modify state
    }
  }finally{
    writeLock.unlock()
  }
}

在寫鎖中作為任何自尊並發程序檢查所需的狀態。

不應在調試/監控/快速失敗檢測之外使用 HoldCount。

我想ReentrantLock的動機是遞歸遍歷樹:

public void doSomething(Node node) {
  // Acquire reentrant lock
  ... // Do something, possibly acquire write lock
  for (Node child : node.childs) {
    doSomething(child);
  }
  // Release reentrant lock
}

你不能重構你的代碼以將鎖處理移到遞歸之外嗎?

public void doSomething(Node node) {
  // Acquire NON-reentrant read lock
  recurseDoSomething(node);
  // Release NON-reentrant read lock
}

private void recurseDoSomething(Node node) {
  ... // Do something, possibly acquire write lock
  for (Node child : node.childs) {
    recurseDoSomething(child);
  }
}

那么,我們是否期望 Java 僅在此線程尚未對 readHoldCount 做出貢獻時才增加讀取信號量計數? 這意味着與僅維護 int 類型的 ThreadLocal readholdCount 不同,它應該維護 Integer 類型的 ThreadLocal Set(維護當前線程的 hasCode)。 如果這沒問題,我建議(至少現在)不要在同一個類中調用多個讀取調用,而是使用一個標志來檢查當前對象是否已經獲得了讀取鎖。

private volatile boolean alreadyLockedForReading = false;

public void lockForReading(Lock readLock){
   if(!alreadyLockedForReading){
      lock.getReadLock().lock();
   }
}

ReentrantReadWriteLock的文檔中找到。 它清楚地表明,當嘗試獲取寫鎖時,讀取器線程永遠不會成功。 根本不支持您嘗試實現的目標。 必須在獲取寫鎖之前釋放讀鎖。 降級還是有可能的。

重入性

此鎖允許讀取器和寫入器以 {@link ReentrantLock} 的樣式重新獲取讀或寫鎖。 在寫線程持有的所有寫鎖都被釋放之前,不允許非可重入讀者。

此外,作者可以獲取讀鎖,但反之則不行。 在其他應用程序中,當在調用或回調在讀鎖下執行讀取的方法期間持有寫鎖時,可重入可能很有用。 如果讀者試圖獲取寫鎖,它將永遠不會成功。

來自上述來源的示例用法:

 class CachedData {
   Object data;
   volatile boolean cacheValid;
   ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

   void processCachedData() {
     rwl.readLock().lock();
     if (!cacheValid) {
        // Must release read lock before acquiring write lock
        rwl.readLock().unlock();
        rwl.writeLock().lock();
        // Recheck state because another thread might have acquired
        //   write lock and changed state before we did.
        if (!cacheValid) {
          data = ...
          cacheValid = true;
        }
        // Downgrade by acquiring read lock before releasing write lock
        rwl.readLock().lock();
        rwl.writeLock().unlock(); // Unlock write, still hold read
     }

     use(data);
     rwl.readLock().unlock();
   }
 }

在 ReentrantReadWriteLock 上使用“公平”標志。 “公平”意味着鎖定請求以先到先得的方式提供。 您可能會遇到性能下降,因為當您發出“寫”請求時,所有后續“讀”請求都將被鎖定,即使它們可以在預先存在的讀鎖仍被鎖定時提供服務。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM