同期ロックと ReentrantLock の違いは何ですか?

序文

ソフトウェアの同時実行性は現代のソフトウェア開発の基本機能となっており、慎重に設計された Java の効率的な同時実行メカニズムは、大規模なアプリケーションを構築するための基盤の 1 つです。

このブログ投稿の焦点は、synchronized と ReentrantLock の違いは何でしょうか? 同期が最も遅いという人もいますが、本当ですか?  

よくある質問

synchronizedこれは Java の組み込み同期メカニズムであるため、組み込みロックと呼ぶ人もいます。相互に排他的なセマンティクスと可視性を提供します。スレッドが現在のロックを取得すると、取得しようとしている他のスレッドはそこで待機またはブロックすることしかできません。

Java 5 より前では、synchronized が唯一の同期メソッドでした。コード内では、synchronized を使用してメソッドを変更したり、特定のコード ブロックで使用したりできます。本質的に、synchronized メソッドは、メソッドのすべてのステートメントをラップするのと同じです。同期されたブロック。

ReentrantLockは、通常、再入可能ロックとして翻訳され、Java 5 によって提供されるロック実装です。そのセマンティクスは基本的に同期と同じです。再エントリ ロックは、コードを通じて lock() メソッドを直接呼び出すことによって取得されるため、コードの記述がより柔軟になります。同時に、ReentrantLock は、公平性の制御や定義された条件の使用など、同期では実現できない多くの詳細な制御を実現できる実用的なメソッドを多数提供します。ただし、コーディング時に注意が必要で、明示的にunlock()メソッドを呼び出して解放しないと常にロックが保持されてしまいます。

synchronized と ReentrantLock のパフォーマンスは一般化できません。synchronized の初期バージョンでは、多くのシナリオでパフォーマンスに大きな違いがあります。後続のバージョンでは多くの改善が加えられており、競合が少ないシナリオではパフォーマンスが ReentrantLock よりも優れている可能性があります。  

具体分析

並行プログラミングに関しては、企業や面接官によって面接のスタイルが異なり、大手企業では関連する仕組みの拡張や基礎的な仕組みについて聞き続けることを好む企業もあれば、実践的な観点からスタートすることを好む企業もあり、ある程度の忍耐力が必要です。同時プログラミングの準備中。

同時実行の基本ツールの 1 つとして、ロックは少なくとも以下を習得する必要があります。

  • スレッドセーフとは何かを理解します。
  • 同期、ReentrantLock、その他のメカニズムの基本的な使用法とケース。

さらに一歩進めるには、次のものが必要です。

  • synchronized ロックと ReentrantLock の基礎となる実装をマスターし、ロックの拡張と劣化を理解し、スキュー ロック、スピン ロック、軽量ロック、重量ロックなどの概念を理解します。
  • 同時実行パッケージの java.util.concurrent.lock のさまざまな実装とケース スタディをマスターします。  

実践的な分析

まず、スレッド セーフとは何かを理解する必要があります。

Brain Goetz などの専門家によって書かれた「Java Concurrency in Practice」では、スレッド セーフはマルチスレッド環境における正確性の概念、つまり、マルチスレッド環境における共有および変更可能な状態の正確性を保証するという概念です。ここでプログラムに反映されるステータスは、実際にはデータとみなすことができます。

別の観点から見ると、状態が共有されていないか変更可能でない場合、スレッドの安全性の問題はなく、スレッドの安全性を確保する 2 つの方法が推測できます。

  • カプセル化: カプセル化を通じて、オブジェクトの内部状態を非表示にして保護できます。
  • 不変性: これは、final と immutable に当てはまります。Java 言語には現在、真のネイティブな不変性がありませんが、将来導入される可能性があります。

スレッドセーフでは、いくつかの基本的な特性を確保する必要があります。

  • 原子性とは、簡単に言うと、関連する操作が途中で他のスレッドによって干渉されないことを意味し、通常は同期メカニズムによって実現されます。
  • 可視性とは、スレッドが共有変数を変更するときに、そのステータスを他のスレッドがすぐに知ることができることを意味します。これは通常、スレッドのローカル ステータスをメイン メモリに反映すると解釈されます。Volatile は可視性を確保する責任があります。
  • 順序性により、スレッド内のシリアル セマンティクスが確保され、命令の再配置が回避されます。

少しわかりにくいかもしれないので、次のコード スニペットを見て、アトミック性の要件がどこに反映されているかを分析してみましょう。この例では、2 つの値を取得して比較することにより、共有状態に対する 2 つの操作をシミュレートします。

コンパイルして実行すると、2 つのスレッドの同時実行性が低いだけで、前者と後者が等しくない状況が非常に簡単に発生することがわかります。これは、2 つの値の取得プロセス中に他のスレッドがsharedStateを変更した可能性があるためです。

public class ThreadSafeSample {
  public int sharedState;
  public void nonSafeAction() {
      while (sharedState < 100000) {
          int former = sharedState++;
          int latter = sharedState;
          if (former != latter - 1) {
              System.out.printf("Observed data race, former is " +
                      former + ", " + "latter is " + latter);
          }
      }
  }

  public static void main(String[] args) throws InterruptedException {
      ThreadSafeSample sample = new ThreadSafeSample();
      Thread threadA = new Thread(){
          public void run(){
              sample.nonSafeAction();
          }
      };
      Thread threadB = new Thread(){
          public void run(){
              sample.nonSafeAction();
          }
      };
      threadA.start();
      threadB.start();
      threadA.join();
      threadB.join();
  }
}
复制代码

特定の実行の結果は次のとおりです。

Observed data race, former is 9851, latter is 9853
复制代码

2 つの割り当てプロセスを synchronized で保護し、これを相互排他的なユニットとして使用して、他のスレッドがsharedStateを同時に変更するのを防ぎます。

synchronized (this) {
  int former = sharedState ++;
  int latter = sharedState;
  // …
}
复制代码

javap を使用して逆コンパイルすると、monitorenter/monitorexit ペアを使用して同期セマンティクスを実現し、同様のフラグメントを確認できます。

11: astore_1
12: monitorenter
13: aload_0
14: dup
15: getfield    #2                // Field sharedState:I
18: dup_x1
…
56: monitorexit
复制代码

コード内で synchronized を使用すると非常に便利です。静的メソッドの変更に使用する場合、次のコードを使用してメソッド本体を含めることと同じです。

synchronized (ClassName.class) {}
复制代码

ReentrantLock を見てみましょう。再突入とは何なのか疑問に思われるかもしれません。これは、スレッドがすでに取得済みのロックを取得しようとすると、取得アクションが自動的に成功することを意味します。これは、ロック取得の粒度の概念であり、ロックは数に基づくのではなくスレッド単位で保持されます。通話の数。Java ロックの実装では、pthread の動作と区別するために再入性が強調されています。

リエントラント ロックは公平性 (公平性) に設定でき、リエントラント ロックを作成するときに公平かどうかを選択できます。

ReentrantLock fairLock = new ReentrantLock(true);
复制代码

ここでのいわゆる公平性とは、競争シナリオにおいて、公平性が真である場合、最も長く待機していたスレッドにロックが与えられる傾向があることを意味します。公平性は、スレッドの「スターベーション」(個々のスレッドが長時間ロックを待機するのにロックを取得できない状態) の発生を減らす方法です。

同期を使用すると、公平な選択がまったくできなくなり、常に不公平になります。これは、主流のオペレーティング システムのスレッド スケジューリングの選択でもあります。一般的なシナリオでは、公平性は想像されているほど重要ではない可能性があり、Java のデフォルトのスケジューリング ポリシーによって「スターベーション」が発生することはほとんどありません。同時に、公平性を確保すると追加のオーバーヘッドが発生し、当然スループットのある程度の低下につながります。したがって、プログラムに本当に公平性が必要な場合にのみ指定する必要があることをお勧めします。

ロックに入る前に日々のコーディングの観点から学んでみましょう。ロックを確実に解放するには、すべての lock() アクションを try-catch-finally に即座に対応させることをお勧めします。典型的なコード構造は次のとおりです。これは良い習慣です。

ReentrantLock fairLock = new ReentrantLock(true);// 这里是演示创建公平锁,一般情况不需要。
fairLock.lock();
try {
  // do something
} finally {
   fairLock.unlock();
}
复制代码

synchronized と比較すると、ReentrantLock は通常のオブジェクトと同じように使用できるため、ReentrantLock が提供するさまざまな便利なメソッドを使用して、細かい同期操作を実行したり、synchronized では表現が難しい次のようなユースケースを実装したりすることもできます。

  • タイムアウト付きでロックを取得しようとします。
  • ロックを取得するために並んで待機しているスレッドまたは特定のスレッドが存在するかどうかを判断できます。
  • 割り込み要求に応答できます。
  • ...

ここで特に強調したいのは条件変数(java.util.concurrent.Condition) で、ReentrantLock が synchronized の代替である場合、Condition は wait、notify、notifyAll などの操作を対応するオブジェクトに変換し、複雑でわかりにくい同期操作を直感的なものに変換します。そして制御可能なオブジェクトの動作。

条件変数の最も典型的なアプリケーション シナリオは、標準クラス ライブラリの ArrayBlockingQueue です。

以下のソースコードを参照してください。まず、リエントリーロックを通じて条件変数を取得します。


/** Condition for waiting takes */
private final Condition notEmpty;

/** Condition for waiting puts */
private final Condition notFull;
 
public ArrayBlockingQueue(int capacity, boolean fair) {
  if (capacity <= 0)
      throw new IllegalArgumentException();
  this.items = new Object[capacity];
  lock = new ReentrantLock(fair);
  notEmpty = lock.newCondition();
  notFull =  lock.newCondition();
}
复制代码

2 つの条件変数が同じリエントラント ロックから作成され、以下の take メソッドなどの特定の操作で使用され、条件が満たされるかどうかを判断して待機します。

public E take() throws InterruptedException {
  final ReentrantLock lock = this.lock;
  lock.lockInterruptibly();
  try {
      while (count == 0)
          notEmpty.await();
      return dequeue();
  } finally {
      lock.unlock();
  }
}
复制代码

キューが空の場合、実行しようとしているスレッドの正しい動作は、直接返すのではなく、エンキューが発生するのを待つことです。これが BlockingQueue のセマンティクスです。このロジックは、条件付き notEmpty を使用してエレガントに実装できます。

では、キューへの参加によって後続の取得操作が確実にトリガーされるようにするにはどうすればよいでしょうか? エンキューの実装を見てください。

private void enqueue(E e) {
  // assert lock.isHeldByCurrentThread();
  // assert lock.getHoldCount() == 1;
  // assert items[putIndex] == null;
  final Object[] items = this.items;
  items[putIndex] = e;
  if (++putIndex == items.length) putIndex = 0;
  count++;
  notEmpty.signal(); // 通知等待的线程,非空条件已经满足
}
复制代码

signal/awaitの組み合わせにより、条件判定スレッドと通知待ちスレッドが完了し、ステータスの流れが非常にスムーズに完了します。signal と await をペアで呼び出すことが非常に重要であることに注意してください。そうしないと、await アクションのみがあると想定され、スレッドは割り込まれるまで待機することになります。

パフォーマンスの観点から見ると、synchronized の初期の実装は比較的非効率的でしたが、ReentrantLock と比較すると、ほとんどのシナリオのパフォーマンスは大きく異なります。ただし、Java 6 では多くの改善が加えられており、パフォーマンスの比較を参照してください。競争が激しい状況では、ReentrantLock には依然として一定の利点があります。次回の講義での詳細な分析は、パフォーマンスの違いの根本的な理由を理解するのにさらに役立ちます。ほとんどの場合、パフォーマンスについて心配する必要はありませんが、コード記述構造の利便性と保守性を考慮してください。  

追記

Javaについては以上です 。synchronized と ReentrantLock の違いは何ですか?  すべてのコンテンツ。

スレッドセーフとは何か、synchronizedとReentrantLockの比較と分析、条件変数などをケースコードで紹介します。

おすすめ

転載: blog.csdn.net/2301_76607156/article/details/130525557