为什么 lambda 表达式中使用的变量应该是最终变量或有效最终变量

Thu*_*ght 5 java concurrency lambda

这个问题之前已经在这里被问过\n
\n我的问题为什么在这里得到回答\n
\n但是我对答案有一些疑问。\n提供的答案提到-

\n
\n
\n

尽管其他答案证明了该要求,但它们没有解释该要求存在的原因。

\n
\n
\n
\n

JLS 在\xc2\xa715.27.2中提到了原因:

\n
\n
\n
\n

对有效最终变量的限制禁止访问动态更改的局部变量,捕获这些变量可能会引入并发问题。

\n
\n
\n
\n

为了降低错误风险,他们决定确保捕获的变量永远不会发生变化。\n我对这会导致并发问题的说法感到困惑。\n

\n
\n

我在Baeldung上阅读了有关并发问题的文章,但我仍然对它如何导致并发问题感到有点困惑,有人可以帮我举个例子吗。\n提前致谢。

\n

Rav*_*ala 5

创建 lambda 表达式的实例时,它引用的封闭范围内的任何变量都会复制到其中。现在,假设如果允许修改,现在您正在使用该副本中的陈旧值。另一方面,假设在 lambda 内部修改了副本,并且封闭范围内的值仍然没有更新,从而导致不一致。因此,为了防止这种情况发生,语言设计者施加了这个限制。这可能也会让他们的生活变得更轻松。可以在此处找到匿名内部类的相关答案。

另一点是,您将能够传递 lambda 表达式,如果它被转义并且不同的线程执行它,而当前线程正在更新相同的局部变量,那么也会出现一些并发问题。


Sla*_*law 3

我想在这个答案的前言中说,我下面显示的内容实际上并不是 lambda 的实现方式。 java.lang.invoke.LambdaMetafactory 如果我没记错的话,实际的实施涉及到。我的回答利用了一些不准确的地方来更好地证明这一点。

\n
\n

假设您有以下内容:

\n
public static void main(String[] args) {\n  String foo = "Hello, World!";\n  Runnable r = () -> System.out.println(foo);\n  r.run();\n}\n
Run Code Online (Sandbox Code Playgroud)\n

请记住,lambda 表达式是声明函数接口实现的简写。lambda 主体是所述功能接口的单个​​抽象方法的实现。在运行时创建一个实际的对象。所以上面的结果是一个类实现了Runnable.

\n

现在,上面的 lambda 主体引用了封闭方法中的局部变量。作为 lambda 表达式的结果创建的实例“捕获”该局部变量的值。几乎(但不是真的)就像您有以下内容:

\n
public static void main(String[] args) {\n  String foo = "Hello, World!";\n\n  final class GeneratedClass implements Runnable {\n    \n    private final String generatedField;\n\n    private GeneratedClass(String generatedParam) {\n      generatedField = generatedParam;\n    }\n\n    @Override\n    public void run() {\n      System.out.println(generatedField);\n    }\n  }\n\n  Runnable r = new GeneratedClass(foo);\n  r.run();\n}\n
Run Code Online (Sandbox Code Playgroud)\n

现在应该更容易在这里看到支持并发的问题:

\n
    \n
  1. 局部变量不被视为“共享变量”。Java 语言规范的 \xc2\xa717.4.1中对此进行了说明:

    \n
    \n

    可以在线程之间共享的内存称为共享内存或堆内存。

    \n

    所有实例字段、静态字段和数组元素都存储在堆内存中。在本章中,我们使用术语变量来指代字段和数组元素。

    \n

    局部变量 (\xc2\xa714.4)、形式方法参数 (\xc2\xa78.4.1) 和异常处理程序参数 (\xc2\xa714.20) 永远不会在线程之间共享,并且不受内存模型的影响。

    \n
    \n

    也就是说,局部变量不受Java并发规则的覆盖,不能在线程之间共享。

    \n
  2. \n
  3. 在源代码级别,您只能访问局部变量。您看不到生成的字段。

    \n
  4. \n
\n

我认为 Java 可以设计为修改 lambda 主体内部的局部变量仅写入生成的字段,而修改 lambda 主体外部的局部变量仅写入局部变量。但正如您可能想象的那样,这会令人困惑且违反直觉。根据源代码,您将有两个变量似乎是一个变量。更糟糕的是,这两个变量的值可能会有所不同。

\n

另一种选择是不生成字段。但请考虑以下事项:

\n
public static void main(String[] args) {\n  String foo = "Hello, World!";\n  Runnable r = () -> {\n    foo = "Goodbye, World!"; // won\'t compile\n    System.out.println(foo);\n  }\n  new Thread(r).start();\n  System.out.println(foo);\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这里应该发生什么?如果没有生成字段,则局部变量正在被第二个线程修改。但局部变量不能在线程之间共享。因此,这种方法是不可能的,至少在不对 Java 和 JVM 进行重大更改的情况下是不可能的。

\n

因此,据我了解,设计者制定了这样的规则:在这种情况下,局部变量必须是最终的或实际上是最终的,以避免并发问题并使开发人员对深奥的问题感到困惑。

\n

  • 好吧,无论它们如何实现,拥有自己的局部变量副本的要点仍然存在,这是最重要的。另一种方法是 C# 的做法,归根结底是将所有对“局部变量”的访问转换为对堆变量的访问。那么,您可能拥有实际上不是局部变量的局部变量...... (3认同)
  • 重申一下,本地类并不是实现 lambda 的实际机制。我在答案中只使用了本地类,因为它更容易看到并有效地演示了问题。但实际上发生的事情有点复杂,我并不完全理解这一切。如果您感兴趣[本文](https://dzone.com/articles/how-lambdas-and-anonymous-inner-classesaic-work)和[本文](https://www.infoq.com/文章/Java-8-Lambdas-A-Peek-Under-the-Hood/)更详细地介绍了它们如何在 OpenJDK 中实现。 (2认同)