javaのコンカレントパッケージのAtomicクラス
sky ao IT ha ha
これは実際のケースであり、大きな論争を引き起こしました。ストーリーの原因は非常に単純で、単純なカウンターを実装する必要があり、すべての値に1を加えたものであるため、次のコードがあります。
private int counter = 0;
public int getCount ( ) {
return counter++;
}
このカウンターは、外部課金システムとの対話に使用されるsessionIdを生成するために使用されます。もちろん、このsessionIdは、グローバルに一意であり、重複していない必要があります。しかし、残念ながら、上記のコードは最終的に同じIDを生成することが判明したため、一部のリクエストで不可解なエラーが発生します...さらに痛いことに、上記のコードは他の部門によって開発されたツールクラスです。私はそれを呼び出すためにそのjarパッケージを取りました、ソースコードはありません、そして私はそれにそのような低レベルでひどいエラーがあるとは思いませんでした。
sessionIdが繰り返されるため、一部のリクエストは失敗しますが、発生する可能性は非常に低いですが、毎日テストを再現できるとは限りません。請求に関連しているため、エラーの可能性が低くても解決する必要があります。実は、プロジェクト開発の最終段階で、リリース前の最終安定性テストが始まったのですが、7 * 24時間の連続テストでは、テスト開始から数日でこの問題が再発することが多く、トラブルシューティングを担当しました。私の同僚は惨めに投げていました...繰り返し検索した後、誰かがついに彼らがここに来たのではないかと疑い、jarパッケージを逆コンパイルし、上記のコードが故障しているのを見ました。
この低レベルのエラーは、javaの基本的な知識に起因します
。i++または++ iに関係なく、++操作はアトミック操作ではありません。
また、非アトミック操作では、マルチスレッド同時実行下でスレッドの安全性の問題が発生します。ここに簡単な説明があります。上記の「++」演算子には、原則として、実際には次のものが含まれています。1を加算してから新しい値を計算し、次にこの新しい値を元の変数に割り当てて、元の値を返します。以下のコードと同様
private int counter = 0;
public int getCount ( ) {
int result = counter;
int newValue = counter + 1; // 1. 计算新值
counter = newValue; // 2. 将新值赋值给原变量
return result;
}
複数のスレッドが同時に発生している場合、2つのスレッドが同時にgetCount()メソッドを呼び出すと、同じカウンター値を取得する可能性があります。安全性を確保するために、最も簡単なメソッドの1つは、getCount()メソッドで同期することです。
private int counter = 0;
public synchronized int getCount ( ) {
return counter++;
}
このようにして、++演算子の非アトミックな性質によって引き起こされる同時実行の危険を回避できます。
このケースを少し拡張してみましょう。ここでの操作がアトミックである場合、同期と安全性のない同時アクセスは可能ですか?このコードを少し変更します。
private int something = 0;
public int getSomething ( ) {
return something;
}
public void setSomething (int something) {
this.something = something;
}
getSomething()メソッドとsetSomething()メソッドに同時にアクセスする複数のスレッドがあるとすると、スレッドがsetSomething()メソッドを呼び出して新しい値を設定すると、getSomething()を呼び出す他のメソッドはすぐに新しい値を読み取ることができますか? ?ここで、「this.something = something;」はint型への割り当てです。Java言語の仕様によると、intへの割り当てはアトミック操作です。上記の場合、非アトミック操作の隠れた危険はありません。
しかし、ここには「メモリの可視性」と呼ばれる重要な問題がまだあります。ここでは、Javaメモリモデルに関する一連の知識が含まれています。スペースに限りがあるため、詳細については説明しません。これらの知識ポイントがわからない場合は、自分で情報を確認できます。最も簡単な方法は、これら2つのキーワード「javaメモリモデル」、「java」をグーグルで検索することです。メモリの可視性」。または、この投稿「Javaスレッドの安全性の概要」を参照できます。
ここで「メモリの可視性」の問題を解決するには、2つの方法があります。1つは、synchronizedキーワードを引き続き使用することです。コードは、次のとおりです。
private int something = 0;
public synchronized int getSomething ( ) {
return something;
}
public synchronized void setSomething (int something) {
this.something = something;
}
另一个是使用volatile 关键字,
private volatile int something = 0;
public int getSomething ( ) {
return something;
}
public void setSomething (int something) {
this.something = something;
}
volatileは軽量の同期であり、マルチスレッドメモリの可視性のみを保証できますが、マルチスレッドの正常な実行は保証できないため、volatileキーワードを使用したソリューションはパフォーマンスの点ではるかに優れています。したがって、オーバーヘッドは同期よりもはるかに小さくなります。
ケースの最初に戻りましょう。getCount()メソッドの直前に同期を追加する変更されたメソッドを使用しているため、非アトミック操作によって引き起こされるマルチスレッド実行の順序を回避するだけでなく、「ちなみに」メモリの可視性の問題を解決しました。
さて、続行できます。前述のように、getCount()メソッドの前に同期を追加することで問題を解決できますが、実際には、jdk5.0以降に導入されたコンカレントパッケージで提供されるアトミッククラスを使用するより便利な方法があります。 java.util.concurrent.atomic.Atomic ***、AtomicInteger、AtomicLongなど。
private AtomicInteger counter = new AtomicInteger(0);
public int getCount ( ) {
return counter.incrementAndGet();
}
Atomicクラスは、データ操作のスレッドの安全性を保証するだけでなく、incrementAndGet()、getAndIncrement、addAndGet()、getAndAdd()などの使いやすい明確なセマンティクスを持つ一連のメソッドも提供します。さらに重要なことに、Atomicクラスは単純な同期パッケージではありません。その内部実装は、単に同期を使用するだけでなく、CAS(比較とスワップ)+揮発性のより効率的な方法であるため、同期の高いオーバーヘッドと実行効率を回避できます。大幅に改善されました。スペースの制限により、「CAS」の原理についてはここでは説明しません。
したがって、パフォーマンス上の理由から、synchronizedキーワードに基づいてコード実装を作成するのではなく、可能な限りAtomicクラスを使用することを強くお勧めします。
最後に、要約すると、この投稿では、いくつかの問題について説明しました
。1。++操作はアトミック操作ではありません
2.非アトミック操作にはスレッドの安全性の問題があります
3.同時実行時のメモリの可視性
4.アトミッククラスはCAS + volatileを介して実現できます同期して推奨するよりも効率的です