0

ReentrantReadWriteLock多数の異なる読み取りおよび書き込み操作を持つかなり複雑なデータ構造へのアクセスを制御しようとしているときに、いくつかの密接に関連する質問がポップアップしました。ドキュメントの例に従って、読み取りロックと書き込みロックをフェッチし、それらを使用して (私が知る限り) このデータ構造への同時アクセスを正常に管理しています。ただし、別の問題をデバッグしているときに、同じスレッドで複数の読み取りロックを取得する場合があることに気付きました。これの基本的な理由は、単純なクエリを呼び出す複雑なクエリが多数あるためです (以下の例を参照)。複雑なクエリはトランザクションと考えることができます。つまり、 と へのgetVersion()アクセスの間に書き込みがあってはなりませんdatamyQuery. この現在の解決策は機能しますが、コード内のいくつかの場所で、同じスレッドが複数の読み取りロックを所有することになります。同等の書き込みにはisHeldByCurrentThread()メソッドがあることに気付きましたが、これは奇妙なことに にはありませんreadLock。以下のことがわかりませんでした。

  • 同じスレッドに複数の読み取りロックを設定するのは悪いことですか (スタイル、パフォーマンス、または将来のバグのリスク)?
  • エラーをチェックするために使用WriteLock.isHeldByCurrentThreadするのは不適切ですか (たとえば、書き込みロックを期待しているが何も存在しない、または書き込みロックを解放する条件として)?
  • これが問題である場合、どのように進めるかを決定するためのベストプラクティスはありますか? lockedAquired既にロックを所有しているコードから呼び出される可能性のあるメソッドにブール値を渡すか、それらが一意に取得されるようにするか、または使用する必要がありますReadLock.tryLock()か?

コードサンプルは次のとおりです。

public class GraphWorldModel {

  //unfair RW Lock (default, see javadoc)
  protected final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(false);
  protected final ReentrantReadWriteLock.ReadLock readLock = rwl.readLock();
  protected final ReentrantReadWriteLock.WriteLock writeLock = rwl.writeLock();

  protected long versionID = 0;
  protected Object data = null;

  @Override
  public long getVersion() {
      long result = -1;
      readLock.lock();
      try {
          result = this.versionID;
      } finally {

          readLock.unlock();
      }
      return result;
  }
  public ResultObject myQuery() {
      //do some work
      readLock.lock();
      try {
        long version = getVersion();
        ResultObject result = new ResultObject(this.data, version);
        //note: querying version and result should be atomic!
      } finally {
        readLock.unlock();
      }
      //do some more work
      return result;
  }
}
4

1 に答える 1

1
  • 同じスレッドに複数の読み取りロックを設定するのは悪いことですか (スタイル、パフォーマンス、または将来のバグのリスク)?

私はそれが疑わしいスタイルだと言います。まず、@JBNizet が指摘しているように、既に所有しているロックを再度取得しようとしても問題はありません。ロックはリーダー カウントを 1 つ増やし、最終的なロック解除で減らします。

ただし、メモリ バリア (volatile読み取り) を越えて共有ロック統計を更新する必要があることを意味します。これは、パフォーマンス ヒットを意味します。顕著なパフォーマンスの違いが生じるかどうかは、このコードが実行される頻度によって異なります。

ただし、バグに関しては、大きな落とし穴があります。読み取り専用ロックについて話しているのであれば、バグの可能性はこれ以上ないと思います。実際に二重ロックの問題 (以下を参照) を修正しようとすると、おそらくよりリスクが高くなります。ただし、コードでは、try/finally ロジックにより、ロックが適切に使用され、完了時にロックが解除されることが保証されます。

ただし、読み取りロックと書き込みロックの混在について話している場合は、次のコードがデッドロックすることを理解する必要があります。

ReentrantReadWriteLock rrwl = new ReentrantReadWriteLock();
ReadLock readLock = rrwl.readLock();
WriteLock writeLock = rrwl.writeLock();
readLock.lock();
writeLock.lock();

または、少なくとも他のコードがロックを解除するまで停止しread-lockます。コード内で読み取りロックと書き込みロックを混在させる場合は、二重ロックから保護する必要があります。

  • WriteLock.isHeldByCurrentThread を使用してエラーをチェックするのはよくありません (たとえば、書き込みロックを期待しているが何も存在しない、または書き込みロックを解放する条件として)。

これにより、コードに多くのロジックが追加されますが、これはよりリスクが高くなりますが、パフォーマンス ヒットが大きい場合は価値があるかもしれません。とはいえ、代替案については以下を参照してください。

  • これが問題である場合、どのように進めるかを決定するためのベストプラクティスはありますか? 既にロックを所有しているコードから呼び出される可能性のあるメソッドに lockedAquired ブール値を渡す必要がありますか?それらが一意に取得されるようにしますか?それとも ReadLock.tryLock() を使用する必要がありますか?

私がすることは、 と呼ばれるカーネル メソッドを作成することですgetVersionLocked()。何かのようなもの:

@Override
public long getVersion() {
    readLock.lock();
    try {
        // NOTE: I refactored this to return out of the try/finally which is a fine pattern
        return getVersionLocked();
    } finally {
        readLock.unlock();
    }
}
public ResultObject myQuery() {
    //do some work
    readLock.lock();
    ResultObject result;
    try {
      long version = getVersionLocked();
      result = new ResultObject(this.data, version);
      //note: querying version and result should be atomic!
    } finally {
      readLock.unlock();
    }
    //do some more work
    return result;
}
// NOTE: to call this method, we must already be locked
private long getVersionLocked() {
    return this.versionID;
}

次に、ロックされたバージョンまたはロックされていないバージョンのメソッドを呼び出すかどうかを決定し、readLock()1 回だけ実行します。

getVersionLocked()これは、ロックが保持されていないときに呼び出す可能性があるという点でより危険ですが、許容できるリスクだと思います。

于 2016-12-13T23:53:07.023 に答える