web-dev-qa-db-ja.com

Java ReentrantReadWriteLocks-書き込みロックを安全に取得する方法は?

現在、コードで ReentrantReadWriteLock を使用して、ツリー状の構造を介したアクセスを同期しています。この構造は大きく、小さな部分をときどき修正することで一度に多くのスレッドによって読み取られるため、読み書きのイディオムにうまく適合するようです。この特定のクラスでは、読み取りロックを書き込みロックに上げることができないため、Javadocsに従って書き込みロックを取得する前に読み取りロックを解除する必要があることを理解しています。これまで、このパターンを再入不可のコンテキストで正常に使用しました。

しかし、私が見つけているのは、永久にブロックしないと書き込みロックを確実に取得できないことです。読み取りロックは再入可能であり、実際にそれを使用しているため、単純なコード

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

リードロックをリエントラントに取得した場合はブロックできます。ロック解除の各呼び出しは、保留カウントを減らすだけで、ロックは保留カウントがゼロに達したときにのみ実際に解除されます。

[〜#〜] edit [〜#〜]これを明確にするために、最初はあまりにも説明しすぎたとは思わないので、このクラスには組み込みのロックエスカレーションはなく、読み取りロックを解除して書き込みロックを取得するだけです。私の問題は、他のスレッドが何をしているかに関係なく、getReadLock().unlock()を呼び出しても、実際にthisスレッドがリエントラントに取得した場合、ロックを解除できないことです。この場合、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実装は、特にリエントラントになるように設計されているため、ロックがリエントラントに取得される可能性があるときに、読み取りロックを書き込みロックに引き上げる賢明な方法がありますか?これは、素朴なアプローチが機能しないことを意味する重要な部分です。

63
Andrzej Doyle

私はこれについて少し進歩しました。単にReentrantReadWriteLockの代わりにReadWriteLockとしてロック変数を明示的に宣言することにより(理想的ではないが、おそらくこの場合には必要な悪)、 getReadHoldCount() メソッド。これにより、現在のスレッドの保留数を取得できるため、これまで何度もreadlockを解放できます(その後、同じ数を再取得できます)。したがって、これは、迅速で汚れたテストで示されるように機能します。

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

それでも、これは私ができる最善のことでしょうか?あまりエレガントではありませんが、これを「手動」ではない方法で処理する方法があることを今でも期待しています。

27
Andrzej Doyle

あなたがしたいことが可能になるはずです。問題は、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>
25
Bob

これは古い質問ですが、問題の解決策と背景情報の両方があります。

他の人が指摘しているように、古典的な リーダーライターロック (JDK ReentrantReadWriteLock など)は、読み取りロックを書き込みロックにアップグレードすることを本質的にサポートしていません。

最初に読み取りロックを解除せずに安全に書き込みロックを取得する必要がある場合は、より良い代替方法があります:read-write-update代わりにロックします。

ReentrantReadWrite_Update_Lockを作成し、Apache 2.0ライセンスの下でオープンソースとしてリリースしました here 。また、アプローチの詳細をJSR166 同時実行関心メーリングリストに投稿しました このアプローチは、そのリストのメンバーによる何度かの精査を乗り切りました。

アプローチは非常に単純で、同時実行の関心で述べたように、少なくとも2000年までさかのぼってLinuxカーネルのメーリングリストで議論されたように、このアイデアは完全に新しいものではありません。Netプラットフォームの ReaderWriterLockSlim ロックのアップグレードもサポートします。事実上、この概念は、これまでJava(AFAICT)で実装されていませんでした。

アイデアは、readロックとwriteロックに加えてupdateロックを提供することです。更新ロックは、読み取りロックと書き込みロックの間の中間タイプのロックです。書き込みロックと同様に、一度に1つのスレッドのみが更新ロックを取得できます。しかし、読み取りロックのように、それはそれを保持するスレッドへの読み取りアクセスを許可し、同時に通常の読み取りロックを保持する他のスレッドへのアクセスを許可します。重要な機能は、更新ロックを読み取り専用ステータスから書き込みロックにアップグレードできることです。これは、1つのスレッドのみが更新ロックを保持し、一度にアップグレードできる状態にあるため、デッドロックの影響を受けません。

これにより、ロックのアップグレードがサポートされます。さらに、読み取りスレッドを短時間ブロックするため、read-before-writeアクセスパターンを使用するアプリケーションでは、従来のリーダー/ライターロックよりも効率的です。

使用例は サイト で提供されています。ライブラリは100%のテストカバレッジを持ち、Mavenセントラルにあります。

22
npgall

あなたがしようとしていることは、この方法では不可能です。

問題なく読み取りから書き込みにアップグレードできる読み取り/書き込みロックを持つことはできません。例:

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

ここで、2つのスレッドがその関数に入ると仮定します。 (そして、あなたは並行性を仮定していますよね?そうでなければ、そもそもロックを気にしないでしょう....)

両方のスレッドが同時にを開始し、等速を実行すると仮定します。つまり、どちらも読み取りロックを取得しますが、これは完全に合法です。ただし、両方とも最終的に書き込みロックを取得しようとしますが、いずれも取得できません。他の各スレッドは読み取りロックを保持します。

読み取りロックから書き込みロックへのアップグレードを許可するロックは、定義によりデッドロックを起こしやすいです。申し訳ありませんが、アプローチを変更する必要があります。

8
Steffen Heil

Java 8には、tryConvertToWriteLock(long) AP​​Iを備えたJava.util.concurrent.locks.StampedLockがあります

詳細は http://www.javaspecialists.eu/archive/Issue215.html

4
qwerty

あなたが探しているのはロックのアップグレードであり、標準のJava.concurrent ReentrantReadWriteLockを使用して(少なくとも原子的には)不可能です。あなたの最高のショットは、ロック解除/ロックし、誰もが間に修正を加えていないことを確認します。

あなたがしようとしていること、すべての読み取りロックを邪魔にならないようにすることはあまり良い考えではありません。読み取りロックは、書き込むべきではない理由で存在します。 :)

編集:
Ran Bironが指摘したように、問題が飢v(読み取りロックが常に設定および解放され、ゼロにならない)の場合は、公平なキューを使用してみてください。しかし、あなたの質問はこれがあなたの問題であるとは思われませんでしたか?

編集2:
これで問題が発生しました。実際にスタック上の複数の読み取りロックを取得し、それらを書き込みロック(アップグレード)に変換したいと考えています。 JDK実装では、読み取りロックの所有者を追跡しないため、これは実際には不可能です。あなたが見ることのない読み取りロックを保持している他の人がいる可能性があり、現在のコールスタックは言うまでもなく、スレッドに属する読み取りロックの数がわかりません(つまり、ループが殺されていますall自分自身だけでなく読み取りロック。したがって、書き込みロックは、同時読み取りが完了するのを待たず、手に混乱が生じます。

私は実際に同様の問題を抱えており、誰がどの読み取りロックを取得し、これらを書き込みロックにアップグレードしたかを追跡する独自のロックを作成することになりました。これは、コピーオンライトの種類の読み取り/書き込みロックでもありましたが(リーダーに沿って1人のライターを許可するため)、まだ少し異なりました。

4
falstro

このようなものはどうですか?

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();
        }
    }
}
2
Henry HO

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);
  }
}
1
Olivier

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は、debug/monitor/fast-fail検出を超えて使用しないでください。

1
xss

したがって、Javaは、このスレッドがreadHoldCountにまだ寄与していない場合にのみ読み取りセマフォカウントをインクリメントしますか?これは、int型のThreadLocal readholdCountを維持するだけでなく、ThreadLocal Set of type Integer(現在のスレッドのhasCodeを維持します)。これで問題なければ、(少なくとも)同じクラス内で複数の読み取り呼び出しを呼び出さず、代わりにフラグを使用して読み取りロックが既にあるかどうかを確認することをお勧めします現在のオブジェクトによって取得されるかどうか。

private volatile boolean alreadyLockedForReading = false;

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

ReentrantReadWriteLock のドキュメントにあります。書き込みロックを取得しようとすると、リーダースレッドは決して成功しない、と明確に述べています。達成しようとすることは、単にサポートされていません。書き込みロックを取得する前に、読み取りロックを解除する必要がありますmust。ダウングレードは引き続き可能です。

再入可能

このロックにより、リーダーとライターの両方が{@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();
   }
 }
0
phi