1. 序章

スレッドセーフとその実現方法を紹介する前に。

この記事では、ローカル変数と、それらがスレッドセーフである理由を見ていきます。

2. スタックメモリとスレッド

まず、JVMメモリモデルの簡単な要約から始めましょう。

最も重要なことは、JVMが使用可能なメモリをスタックメモリとヒープメモリに分割することです。 まず、すべてのオブジェクトをヒープに格納します。 次に、ローカルプリミティブとローカルオブジェクト参照をスタックに格納します。

さらに、メインスレッドを含むすべてのスレッドが独自のプライベートスタックを持っていることを理解することが重要です。 したがって、他のスレッドはローカル変数を共有しないため、スレッドセーフになります。

3. 例

次に、ローカルプリミティブと(プリミティブ)フィールドを含む小さなコード例を続けましょう。

public class LocalVariables implements Runnable {
    private int field;

    public static void main(String... args) {
        LocalVariables target = new LocalVariables();
        new Thread(target).start();
        new Thread(target).start();
    }

    @Override
    public void run() {
        field = new SecureRandom().nextInt();
        int local = new SecureRandom().nextInt();
        System.out.println(field + ":" + local);
    }
}

5行目で、LocalVariablesクラスのコピーを1つインスタンス化します。 次の2行で、2つのスレッドを開始します。 どちらも同じインスタンスのrunメソッドを実行します。

run メソッド内で、LocalVariablesクラスのフィールドfieldを更新します。 次に、ローカルプリミティブへの割り当てが表示されます。 最後に、2つのフィールドをコンソールに出力します。

すべてのフィールドのメモリ位置を見てみましょう。

まず、fieldはクラスLocalVariablesのフィールドです。 したがって、それはヒープ上に存在します。 次に、ローカル変数numberはプリミティブです。 したがって、それはスタック上にあります。

printlnステートメントは、2つのスレッドを実行するときに問題が発生する可能性がある場所です

まず、フィールドフィールドは、参照とオブジェクトの両方がヒープ上に存在し、スレッド間で共有されるため、問題が発生する可能性が高くなります。 値はスタック上にあるため、プリミティブlocalは問題ありません。 したがって、JVMはスレッド間でローカルを共有しません。

したがって、実行すると、たとえば、次の出力が得られます。

 821695124:1189444795
821695124:47842893

この場合、実際に2つのスレッドの間に衝突があったことがわかります。 両方のスレッドが同じランダムな整数を生成する可能性は非常に低いため、これを確認します。

4. ラムダ内のローカル変数

ラムダ(および匿名内部クラス)は、メソッド内で宣言でき、メソッドのローカル変数にアクセスできます。 ただし、追加のガードがないと、これは多くの問題につながる可能性があります。

JDK 8より前は、匿名内部クラスは最終的なローカル変数にしかアクセスできないという明示的なルールがありました。 JDK 8では、事実上ファイナルという新しい概念が導入され、ルールの厳格さが緩和されました。 以前にfinalと実質的にfinalを比較しました。また、ラムダを使用する場合の実質的にfinalについても詳しく説明しました。

このルールの結果は、ラムダ内でアクセスされるフィールドはfinalまたは事実上final である必要があり(したがって変更されません)、不変性のためにスレッドセーフになります。

この動作は、次の例で実際に確認できます。

public static void main(String... args) {
    String text = "";
    // text = "675";
    new Thread(() -> System.out.println(text))
            .start();
}

この場合、3行目のコードのコメントを解除すると、コンパイルエラーが発生します。 そのため、ローカル変数textは事実上最終的ではなくなります。

5. 結論

この記事では、ローカル変数のスレッドセーフを調べ、これがJVMメモリモデルの結果であることを確認しました。 また、ラムダと組み合わせたローカル変数の使用法についても調べました。 JVMは、不変性を要求することにより、スレッドセーフを保護します。

いつものように、記事の完全なソースコードは、GitHubから入手できます。