AtomicIntegerを使ってみた

エキサイト株式会社 メディア開発のしばたにえんです。

さっそくですが、下記のコードをご覧ください

class CountTest {

    private int num = 0;

    @SneakyThrows
    void CountThreadNum() {
        int threadNum = 10;
        ExecutorService service = Executors.newFixedThreadPool(threadNum);
        for (int n = 0; n < threadNum; n++) {
            service.submit(() -> {
                for (int m = 0; m < 1000; m++) {
                    num++;
                }
            });
        }
        service.shutdown();
        service.awaitTermination(10, TimeUnit.SECONDS);
        System.out.println(num);
    }
}
// 9818

スレッドを10個でそれぞれ1000回ずつ1を加算していくといった処理になりますが、 この結果は10000にはなりません。 intはアトミックではないためです。

概要

そもそもアトミックとは

コンピュータ上のプログラムの動作で、密接に関連する複数の処理が外部から一つの操作に見え、途中の状態を観測したり介入できない性質を、操作のアトミック性、不可分性などという。

要するに 複数のスレッドからのデータを書き込んでも最終的な値を保証しているというものです。

上記の問題を解決するのはAtomicIntegerです。 AtomicIntgerは多くのスレッドを同時に使用できるIntegerクラスです。

実際に使ってみます

class CountTest {

    // private int num = 0;
    private AtomicInteger atomicInteger = new AtomicInteger();

    @SneakyThrows
    void CountThreadAtomicIntegerNum() {
        int threadNum = 10;
        ExecutorService service = Executors.newFixedThreadPool(threadNum);
        for (int n = 0; n < threadNum; n++) {
            service.submit(() -> {
                for (int m = 0; m < 1000; m++) {
                    atomicInteger.incrementAndGet();
                }
            });
        }
        service.shutdown();
        service.awaitTermination(10, TimeUnit.SECONDS);
        System.out.println(atomicInteger.get());
    }
}
// 10000

期待されている値が取れました。

使い方

AtomicIntegerの使い方は主に二つあります
①カウンターとしての役割をアトミック性を保ちながら行う
②比較処理をアトミック性を保ちながら行う

① 1ずつ加算する場合

void atomicIntegerIncrementAndGet() {
    final List<String> characters = List.of("a", "b", "c", "d", "e", "f", "g", "h");
    final AtomicInteger atomicInteger = new AtomicInteger();
    characters.stream().forEach(
            charcter -> System.out.println(
                    String.format("%s:%s", charcter, atomicInteger.incrementAndGet())
            )
    );
}
// a:1
// b:2
// c:3
// d:4
// e:5
// f:6
// g:7
// h:8

初期値を入れて1ずつ加算する

void atomicIntegerIncrementAndGet() {
    final List<String> characters = List.of("d", "e", "f", "g", "h");
    final AtomicInteger atomicInteger = new AtomicInteger(3);
    characters.stream().forEach(
            charcter -> System.out.println(
                    String.format("%s:%s", charcter, atomicInteger.incrementAndGet())
            )
    );
}
// d:4
// e:5
// f:6
// g:7
// h:8

5ずつ加算する

void atomicIntegerGetAndAdd() {
    final AtomicInteger atomicInteger = new AtomicInteger();
    for (int n = 0; n < 3; n++) {
        System.out.println(atomicInteger.getAndAdd(5));
    }
}
// 0
// 5
// 10

② 比較

void atomicIntegerIncrementAndGetCompare() {
    final List<String> characters = List.of("a", "b", "c", "d", "e", "f", "g", "h");
    int targetNum = 3;
    final AtomicInteger atomicInteger = new AtomicInteger();
    characters.stream().forEach(
            charcter -> {
                if (atomicInteger.compareAndSet(targetNum, atomicInteger.incrementAndGet())) {
                    System.out.println(
                            String.format("%s:%s", charcter, atomicInteger.get())
                    );
                }
            }
    );
}
// c:3

まとめ

マルチスレッドの場合ちょっとしたループ内の加算処理でも上記のようなずれが起きてしまいます。 AtomicIntegerちゃんと使っていきましょう!