blog

JDk8 AtomicInteger ソースコード解析 CAS

i=i+1 や i++, ++i の演算を行うことはスレッド安全でないことを知っていること。 上記のバイトコードの実行プロセスから、カウント+ +プロセスの実装では、jvmは、大まかに2つのステップに...

Sep 12, 2020 · 6 min. read
シェア
  1. AtomicInteger使用シナリオ
  2. AtomicIntegerソースコード解析
  3. CAS

i=i+1 や i++ や ++i の操作はスレッドセーフでないことを知っておいてください。

次のコードはcount++です。

public class MainTest {
 private Integer count=0;
 public void add()
 {
 count++;
 }
}

javapのコンパイル後は以下のようになります:

"Java\jdk1.8.0_211\bin\javap.exe" -c MainTest.class
Compiled from "MainTest.java"
public class com.txs.common.MainTest {
 public com.txs.common.MainTest();
 Code:
 0: aload_0 //参照変数thisをスタックの一番上に押す。
 1: invokespecial #1 // Method java/lang/Object."<init>":()V
 4: return
 public void add();
 Code:
 0: iconst_0 //スタックの先頭に値0をプッシュする。
 1: istore_1 //番目のローカル変数に0を格納する。=0
 2: iload_1 //Unsafeが提供するCASメソッドの基礎となる実装はCPU命令cmpxchgである。=0スタックの先頭にプッシュする
 3: iconst_1//値1をスタックの先頭にプッシュする
 4: iadd //スタックの先頭値1とカウント=0加算して結果をスタックの先頭に押し込む、この時スタックの先頭は1である。
 5: istore_1 //スタックの先頭の値1を2番目のローカル変数に入れ、次にカウントする。=1
 6: return //現在のメソッドから戻る void
}
Process finished with exit code 0

上記のバイトコードの実行プロセスからjvmは、カウント+ +プロセスの実装では、大まかに2つのステップに分かれていることが判明し、最初のステップは、最初のカウントは、0の値を設定し、カウントプラス1になり、カウントに割り当てられます。 スレッドが同時に実行される場合ので、1つ以上のスレッドは、同時に0用のローカル変数カウントから、その結果、実際の結果は、最終的な値よりも小さくなることがあります。

以下は、コードの同時実行の結果、カウントが1000未満になることを示しています。

package com.txs.juc;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
 * juc 自己追加並行性テスト
 */
public class AtomicIntegerTest {
 private static Integer count=0;
 
 private static Integer THREAD_COUNT=1000;
 private static CountDownLatch countDownLatch=new CountDownLatch(THREAD_COUNT);
 public static void main(String[] args) throws Exception {
 for (int i = 0; i < THREAD_COUNT; i++) {
 //1000スレッドを開始する
 new Thread(new Runnable() {
 @Override
 public void run() {
 add();
 countDownLatch.countDown();
 }
 }).start();
 }
 //すべてのスレッドの実行終了を待つ
 countDownLatch.await();
 System.out.println(count);
// System.out.println(atomicInteger);
 }
 public static void add()
 {
 // ++
 try {
 Thread.sleep(1);//スリープ1秒
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 count++;
 }
}

実施結果は以下の通り:

"D:\Program Files\Java\jdk1.8.0_211\bin\java.exe"
917
Process finished with exit code 0
"D:\Program Files\Java\jdk1.8.0_211\bin\java.exe"
877
Process finished with exit code 0
"D:\Program Files\Java\jdk1.8.0_211\bin\java.exe"
897
Process finished with exit code 0

ではこの時点で、count++がスレッドセーフで、実行されるたびに正しい結果が得られるようにするにはどうすればよいでしょうか?1つは、count++メソッドに同期した同期ロックを追加する方法です。しかし、この方法はヘビー級のロックであるため、コードの実行速度が遅くなり、同時実行の量が大きくなると、ロックとロック解除に無駄な時間がかかり、コードは本当に短時間で実行されます。2番目のメソッドは、JUCのアトミック操作を使用することです整数型AtomicIntegerメソッドincrementAndGetインクリメント操作から。

以下は、安全な自己増加コードを実装するAtomicIntegerメソッドです。

package com.txs.juc;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
 * juc 自己追加並行性テスト
 */
public class AtomicIntegerTest {
 
 private static AtomicInteger atomicInteger=new AtomicInteger(0);
 private static Integer THREAD_COUNT=1000;
 private static CountDownLatch countDownLatch=new CountDownLatch(THREAD_COUNT);
 public static void main(String[] args) throws Exception {
 for (int i = 0; i < THREAD_COUNT; i++) {
 //1000スレッドを開始する
 new Thread(new Runnable() {
 @Override
 public void run() {
 addAtomic();
 countDownLatch.countDown();
 }
 }).start();
 }
 //すべてのスレッドの実行終了を待つ
 countDownLatch.await();
// System.out.println(count);
 System.out.println(atomicInteger);
 }
 public static void addAtomic()
 {
 
 try {
 Thread.sleep(1);//スリープ1秒
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 atomicInteger.incrementAndGet();
 }
}

AtomicIntegerメソッドを使ってcount++を実行すると、どのように実行しても結果は常に0010になります。

"D:\Program Files\Java\jdk1.8.0_211\bin\java.exe"
1000
Process finished with exit code 0

では、AtomicIntegerはどのようにしてスレッドセーフを確保し、実装の効率を高めているのでしょうか?AtomicIntegerのソースコードをチェックして分析してみましょう。JDK8 のソースコード。まず、AtomicIntegerクラスの変数とコードの静的ブロックを見てください。

*
 * @since 1.5
 * 
*/
public class AtomicInteger extends Number implements java.io.Serializable {
 private static final long serialVersionUID = 6214790243416807050L;
 // setup to use Unsafe.compareAndSwapInt for updates
 //Unsafec言語を使ってメモリ資源を直接操作するクラスである。
 //このクラスは主にcompareAndSwapIntメソッドとgetIntVolatileメソッドを提供し、メモリ・データを操作する。
 private static final Unsafe unsafe = Unsafe.getUnsafe();
 //フィールド値のオフセットはjavaメモリ上にある。
 private static final long valueOffset;
 static {
 try {
 //objectFieldOffsetオブジェクトAtomicIntegerインスタンスのフィールド値のオフセットを取得する。
 valueOffset = unsafe.objectFieldOffset
 (AtomicInteger.class.getDeclaredField("value"));
 } catch (Exception ex) { throw new Error(ex); }
 }
 //valueつまり、値
 private volatile int value;

次に、AtomicIntegerのインクリメンタル・メソッドincrementAndGetのソースコードを見てみましょう。

 /**
 * Atomically increments by one the current value.
 *
 * @return the updated value
 * //このメソッドはUnsafeのgetAndAddIntメソッドを呼び出す。
 * getAndAddIntこのメソッドは、AtomicIntegerオブジェクトをループしてvalueOffset値のオフセットを取得し、1を足すとvalue値が更新され、古い値の値に戻るというものだ。
 */
 public final int incrementAndGet() {
 return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
 }

unsafe.getAndAddIntメソッドを見てみましょう。

//パラメータ1 var1はフェッチされるオブジェクトである。
//パラメータ2 var2は、メモリオブジェクトvar1のメモリ内オフセットである。
//パラメータ3 var4は1として加算する値である。
public final int getAndAddInt(Object var1, long var2, int var4) {
 //これは..while 
 int var5;
 do {
 //getIntVolatileオブジェクトvar1のオフセットvar2の値を取得するメソッド,
 //メモリからvar2オフセットの値を強制的に取得するこの方法では、使用するプロパティがvolatileで変更され、取得される値がInteger型である必要がある。
 var5 = this.getIntVolatile(var1, var2);
 //compareAndSwapInt このCAS演算回数
 //動作は以下の通りである:
 //var1とvar2はメモリ上のvalueの値を取得するために使用される。
 //メモリから取得した値がvar5と比較され、同じかどうかが確認される。
 //同じように、var5の値を更新するメモリの値について+ var4
 //同じでない場合、false を返し、var5 に代入されたメモリ値を取得するために getIntVolatile メソッドを実行し続ける。
 //実行に成功し、メモリ値をvar5に更新する。+ var4,trueを返すとループからジャンプアウトする。
 //最後に古いメモリアドレスに戻る
 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
 return var5;
 }

CAS演算は、メモリ位置、期待される元の値、および新しい値の3つのオペランドで構成されます。CAS演算が実行されると、メモリ位置の値が期待される元の値と比較され、一致する場合、プロセッサは自動的にその位置の値を新しい値に更新します。ご存知のように、CASはCPUのアトミック命令であり、いわゆるデータの不整合問題を引き起こすことはありません、UnsafeはCPU命令cmpxchgの実装の基礎となるCASメソッドを提供します。

Read next

JDKパフォーマンス監視ツール

1jps main function: Javaプロセスのリスト 1.1-v: Java仮想マシンに渡されたパラメータを表示可能 1.2-m: Javaプロセスに渡されたパラメータをエクスポートするために使用 1.3-l: メイン関数のフルパスをエクスポートするために使用可能

Sep 12, 2020 · 6 min read