70

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

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

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

再入可能に読み取りロックを取得した場合、ブロックできます。ロックを解除するための各呼び出しは、保持カウントを減らすだけであり、ロックは保持カウントがゼロになったときにのみ実際に解放されます。

最初はあまりうまく説明できなかったと思うので、これを明確にするために編集してください-このクラスには組み込みのロックエスカレーションがなく、読み取りロックを解放して書き込みロックを取得するだけでよいことを認識しています。私の問題は、他のスレッドが何をしているかに関係なく、このスレッドが再入可能に取得した場合、呼び出し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 の実装は再入可能になるように特別に設計されているため、ロックが再入可能に取得される可能性がある場合に、読み取りロックを書き込みロックに昇格させる賢明な方法は確かにありますか? これは、単純なアプローチが機能しないことを意味する重要な部分です。

4

12 に答える 12

30

これは古い質問ですが、問題の解決策と背景情報の両方を次に示します。

他の人が指摘しているように、従来のリーダー/ライター ロック(JDK ReentrantReadWriteLockなど) は、デッドロックの影響を受けやすいため、読み取りロックから書き込みロックへのアップグレードを本質的にサポートしていません。

最初に読み取りロックを解放せずに書き込みロックを安全に取得する必要がある場合は、より良い代替手段があります。代わりに、読み取り-書き込み-更新ロックを調べてください。

私は ReentrantReadWrite_Update_Lock を作成し、Apache 2.0 ライセンスの下でオープン ソースとしてリリースしまし。また、アプローチの詳細を JSR166 concurrency-interest メーリング リストに投稿しましたが、このアプローチは、そのリストのメンバーによる精査を何度か繰り返しました。

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

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

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

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

于 2013-09-13T11:31:54.160 に答える
29

私はこれで少し進歩しました。ロック変数ReentrantReadWriteLockを単に a ではなくa として明示的に宣言することで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();
}

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

于 2009-01-21T10:58:19.697 に答える
25

やりたいことはできるはずです。問題は、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);

この機能がなければ、一連のデータを一貫して読み取り、読み取った内容に基づいて何かを書き込むシリアライズ可能なトランザクションを実装する唯一の方法は、開始する前に書き込みが必要になることを予測することです。書くべきことを書く前に読む。これは、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>
于 2011-01-29T02:02:11.690 に答える
11

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

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

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

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

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

読み取りロックを書き込みロックにアップグレードできるロックは、定義上、デッドロックが発生しやすくなります。申し訳ありませんが、アプローチを変更する必要があります。

于 2010-04-04T23:45:44.707 に答える
6

Java 8java.util.concurrent.locks.StampedLock にはtryConvertToWriteLock(long)APIが追加されました

詳細はhttp://www.javaspecialists.eu/archive/Issue215.htmlをご覧ください

于 2015-02-24T06:05:47.633 に答える
4

あなたが探しているのはロックのアップグレードであり、標準の java.concurrent ReentrantReadWriteLock を使用して (少なくとも原子的には) 不可能です。あなたの最善の策は、ロックを解除してロックし、その間に誰も変更を加えていないことを確認することです。

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

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

編集 2:
私は今、あなたの問題を参照してください。スタック上で実際に複数の読み取りロックを取得しており、それらを書き込みロック (アップグレード) に変換したいと考えています。JDK 実装では、読み取りロックの所有者を追跡しないため、これは実際には不可能です。表示されない読み取りロックを保持している他のユーザーが存在する可能性があり、現在の呼び出しスタックは言うまでもなく、スレッドに属する読み取りロックの数がわからない (つまり、ループがすべての読み取りロックを強制終了している、自分だけのものではないため、書き込みロックは同時読み取りが終了するのを待たず、手が混乱することになります)

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

于 2009-01-21T11:01:38.283 に答える
2

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

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();
        }
    }
}
于 2014-07-11T07:55:31.893 に答える
2

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 検出を超えて使用しないでください。

于 2010-03-21T13:30:10.290 に答える
1

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

private volatile boolean alreadyLockedForReading = false;

public void lockForReading(Lock readLock){
   if(!alreadyLockedForReading){
      lock.getReadLock().lock();
   }
}
于 2011-12-02T20:52:12.363 に答える
1

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);
  }
}
于 2009-01-21T12:15:46.183 に答える
-1

ReentrantReadWriteLock で "fair" フラグを使用します。「公正」とは、ロック要求が先着順で処理されることを意味します。「書き込み」リクエストを発行すると、既存の読み取りロックがまだロックされている間に処理された可能性がある場合でも、後続の「読み取り」リクエストがすべてロックされるため、パフォーマンスが低下する可能性があります。

于 2009-01-21T10:59:18.117 に答える