Lambdasで使用されるローカル変数が最終または実質的に最終でなければならないのはなぜですか?

1. 前書き

Java 8はラムダを提供し、関連により、「実質的に最終的な」変数の概念を提供します。 ラムダでキャプチャされたローカル変数が最終または事実上最終でなければならない理由を疑問に思ったことはありませんか?
さて、https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.27.2 [JLS]は、「制限最終的な変数を効果的に使用すると、動的に変化するローカル変数へのアクセスが禁止されます。ローカル変数をキャプチャすると、同時実行の問題が発生する可能性があります。」
次のセクションでは、この制限をさらに深く掘り下げ、Javaがこの制限を導入した理由を見ていきます。 *シングルスレッドおよびコンカレントアプリケーションにどのように影響するかを示すための例を示します。また、*この制限を回避するための一般的なアンチパターンをデバンクします*。

2. ラムダの捕獲

ラムダ式では、外部スコープで定義された変数を使用できます。 これらのラムダを「ラムダのキャプチャ」と呼びます。 静的変数、インスタンス変数、ローカル変数をキャプチャできますが、*ローカル変数のみが最終または実質的に最終でなければなりません*。
以前のJavaバージョンでは、匿名の内部クラスがそれを囲むメソッドにローカルな変数をキャプチャしたときにこれに遭遇しました。コンパイラが満足するためには、ローカル変数の前に_final_キーワードを追加する必要がありました。
少しの構文上のシュガーとして、コンパイラーは__final __keywordがなくても参照がまったく変わらない、つまり実質的に最終的な状況を認識することができます。 *変数を最終宣言するとコンパイラが文句を言わなければ、変数は事実上最終であると言えます。*

3. ラムダをキャプチャする際のローカル変数

簡単に言えば、*これはコンパイルされません:*
Supplier<Integer> incrementer(int start) {
  return () -> start++;
}
_ start _はローカル変数であり、ラムダ式内で変更しようとしています。
これがコンパイルされない基本的な理由は、ラムダが_start_の値を取得し、そのコピーを作成することであるためです。パラメータ。
しかし、なぜコピーを作成するのですか? さて、メソッドからラムダを返していることに注意してください。 したがって、_start_メソッドパラメータがガベージコレクションを取得するまで、ラムダは実行されません。 Javaは、このラムダをこのメソッドの外部に配置するために、_start_のコピーを作成する必要があります。

3.1. 並行性の問題

楽しみのために、Java _did_がローカル変数をキャプチャされた値に何らかの形で接続したままにすることを少し想像してみましょう。
ここで何をすべきか:
public void localVariableMultithreading() {
    boolean run = true;
    executor.execute(() -> {
        while (run) {
            // do operation
        }
    });

    run = false;
}
これは無害に見えますが、「可視性」という陰湿な問題があります。 各スレッドが独自のスタックを取得することを思い出してください。それで、他のスタックの__run __variableへの変更を_while_ループが_sees_するようにするにはどうすればよいでしょうか? 他のコンテキストでの答えは、__ synchronized __blocksまたは__volatile __keywordを使用することです。
ただし、* Javaは実質的に最終的な制限を課すため、このような複雑さを心配する必要はありません*。

4. ラムダをキャプチャする際の静的変数またはインスタンス変数

前の例では、ラムダ式での静的変数またはインスタンス変数の使用と比較すると、いくつかの疑問が生じる可能性があります。
_start_変数をインスタンス変数に変換するだけで、最初の例をコンパイルできます。
private int start = 0;

Supplier<Integer> incrementer() {
    return () -> start++;
}
しかし、ここで_start_の値を変更できるのはなぜですか?
簡単に言えば、それはメンバー変数が保存される場所に関するものです。 ローカル変数はスタック上にありますが、メンバー変数はヒープ上にあります。 ヒープメモリを扱っているため、コンパイラは、ラムダが_start._の最新の値にアクセスできることを保証できます。
同じことを行うことで、2番目の例を修正できます。
private volatile boolean run = true;

public void instanceVariableMultithreading() {
    executor.execute(() -> {
        while (run) {
            // do operation
        }
    });

    run = false;
}
__run __variableは、__volatile __keywordを追加したため、別のスレッドで実行された場合でもラムダに表示されます。
一般的に、インスタンス変数をキャプチャする場合、最終的な変数_this_をキャプチャすると考えることができます。 とにかく、*コンパイラが文句を言わないという事実は、特にマルチスレッド環境では、予防措置を講じるべきではないという意味ではありません*。

5. 回避策を避ける

ローカル変数の制限を回避するために、誰かが変数ホルダーを使用してローカル変数の値を変更することを考えるかもしれません。
配列を使用して、シングルスレッドアプリケーションに変数を格納する例を見てみましょう。
public int workaroundSingleThread() {
    int[] holder = new int[] { 2 };
    IntStream sums = IntStream
      .of(1, 2, 3)
      .map(val -> val + holder[0]);

    holder[0] = 0;

    return sums.sum();
}
ストリームは各値に2を加算していると考えることができますが、*これはラムダが実行されたときに利用可能な最新の値であるため、実際には0を加算しています。
さらに一歩進んで、別のスレッドで合計を実行しましょう。
public void workaroundMultithreading() {
    int[] holder = new int[] { 2 };
    Runnable runnable = () -> System.out.println(IntStream
      .of(1, 2, 3)
      .map(val -> val + holder[0])
      .sum());

    new Thread(runnable).start();

    // simulating some processing
    try {
        Thread.sleep(new Random().nextInt(3) * 1000L);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }

    holder[0] = 0;
}
ここでどの値を合計しますか? シミュレートされた処理にかかる時間に依存します。 *他のスレッドが実行される前にメソッドの実行を終了できるほど短い場合は6を出力し、そうでない場合は12を出力します*
一般に、この種の回避策はエラーが発生しやすく、予測できない結果を生成する可能性があるため、常に回避する必要があります。

6. 結論

この記事では、ラムダ式が最終または実質的に最終的なローカル変数しか使用できない理由を説明しました。 これまで見てきたように、この制限はこれらの変数の異なる性質と、Javaがそれらをメモリに保存する方法に起因しています。 また、一般的な回避策を使用することの危険性も示しました。
いつものように、例の完全なソースコードが利用可能です。https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-lambdas [GitHub上]。