如何解决为什么实例字段不需要是 final 或有效 final 才能在 lambda 表达式中使用?
我正在用 Java 练习 lambda 表达式。根据 Java SE 16 Lambda Body 的 Oracle 文档,我知道局部变量需要是最终的或有效的最终变量:
使用但未在 lambda 表达式中声明的任何局部变量、形式参数或异常参数必须是 final 或有效 final (§4.12.4),如 §6.5.6.1 中所述。
虽然没有说明原因。搜索我发现了这个类似的问题 Why do variables in lambdas have to be final or effectively final?,其中 StackOverflow 用户“snr”回复了下一个引用:
Java 中的局部变量直到现在都不受竞争条件和可见性问题的影响,因为它们只能被执行声明它们的方法的线程访问。但是一个 lambda 可以从创建它的线程传递到另一个线程,因此如果由第二个线程评估的 lambda 被赋予改变局部变量的能力,那么这种免疫力就会丧失。
这就是我的理解:一个方法一次只能由一个线程(比如说thread_1)执行。这确保该特定方法的局部变量仅由 thread_1 修改。另一方面,可以将 lambda 传递给不同的线程 (thread_2),因此...如果 thread_1 完成 lambda 表达式并继续执行方法的其余部分,则它可能会更改局部变量的值,并且,在同时,thread_2 可能会更改 lambda 表达式中的相同变量。那么,这就是这个限制存在的原因(局部变量需要是 final 或有效 final 的)。
抱歉解释太长了。我做对了吗?
但接下来的问题是:
- 为什么这种情况不适用于实例变量?
- 如果 thread_1 与 thread_2 同时更改实例变量会发生什么情况(即使它们没有执行 lambda 表达式)?
- 是否以其他方式保护实例变量?
我对 Java 没有太多经验。抱歉,如果我的问题有明显的答案。
解决方法
这个问题实际上与线程安全无关。对于为什么总是可以捕获实例变量,有一个简单直接的答案:this
总是有效的最终变量。也就是说,在创建访问实例变量的 lambda 时总是有一个已知的固定对象。请记住,名为 foo
的实例变量总是实际上等效于 this.foo
。
所以
class MyClass {
private int foo;
public void doThingWithLambda() {
doThing(() -> { System.out.println(foo); })
}
}
可以将 lambda 重写为 doThing(() -> System.out.println(this.foo); })
,因此等价于
class MyClass {
private int foo;
public void doThingWithLambda() {
final MyClass me = this;
doThing(() -> { System.out.println(me.foo); })
}
}
...除了 this
已经是最终的并且不需要复制到另一个局部变量(尽管 lambda 会捕获引用)。
当然,所有正常的线程安全警告都适用。如果您的 lambdas 被传递给多个线程并修改变量,那么如果不使用 lambdas,则会发生完全相同的事情,并且除了您的变量的线程安全性(例如,如果它们是 volatile)或者您的lambda 使用其他机制来安全地访问变量。 Lambda 对线程安全没有任何特别之处,它们对实例变量也没有任何特别之处;它们只是捕获对 this
而不是实例变量的引用。
其他答案已经提供了很好的上下文,说明为什么这是 Java 中的限制。我想提供一些关于其他语言在不强制要求将局部变量视为不可变(即 final
)时如何处理此问题的背景知识。
建议的要点是“堆”值(即字段)本质上可以从其他线程访问,而“堆栈”值(即局部变量)本质上只能从声明这些值的方法内部访问。这是真实的。因此,由于字段存储在堆中,因此可以在方法完成后改变它们。相反,方法一结束,堆栈值就会消失。
Java 选择遵守这些语义,因此在方法完成后绝不能修改局部变量。这是一个公平的设计决定。但是,某些语言确实选择在方法退出后允许对局部变量进行突变。那怎么可能呢?
在 C#(我最熟悉的语言,但其他语言(如 JavaScript)也允许这些构造)中,当您在 lambda 中引用局部变量时,编译器检测并在后面场景实际上生成了一个全新的类来存储局部变量。因此,不是在堆栈上声明变量,而是编译器检测到它已在 lambda 内部被引用,因此实例化该类以存储该值。所以这个(在幕后)行为将堆栈值转换为堆值。 (你实际上可以反编译这些代码并查看这些编译器生成的类)
这个决定并非没有代价。实例化一个类只是为了容纳一个整数显然更昂贵。在 Java 中,您可以保证这永远不会发生。在 C# 等语言中,需要仔细推理才能知道您的变量是否已“提升”到该生成的类中。
因此,最终理由成为设计决策之一。在 Java 中,你不能用脚射击自己。在 C# 中,他们认为在大多数情况下,性能影响并不是什么大问题。
也就是说,C# 的决定常常是混淆和错误的根源,尤其是在 for
循环中的循环迭代器变量周围(循环变量 i
可以(并且必须)被改变)和传递给 lambda,如 Eric Lippert 的 blog post 中所述。问题如此严重,以至于他们决定对 foreach
变体的编译器进行(罕见的)重大更改。
另一方面,我很享受在 C# 中的 lamda 内部改变局部变量的自由。但是这两个决定都不是没有代价的。
这个答案绝对不是要提倡任何一个决定,但我认为详细说明其中一些设计选择是值得的。
,实例变量存储在堆空间中,而局部变量存储在堆栈空间中。每个线程维护自己的堆栈,因此局部变量不会在线程之间共享。另一方面,堆空间由所有线程共享,因此多个线程可以修改实例变量。有多种机制可以使数据线程安全,您可以在该平台上找到许多相关讨论。为了完整起见,我在下面引用了 http://web.mit.edu/6.005/www/fa14/classes/18-thread-safety/
的摘录基本上有四种方法可以使变量访问安全 共享内存并发:
- 禁闭。不要在线程之间共享变量。这种想法称为限制,我们今天将对其进行探讨。
- 不变性。使共享数据不可变。我们已经讨论了很多关于不变性的内容,但是还有一些额外的限制 用于我们将在本阅读中讨论的并发编程。
- 线程安全数据类型。将共享数据封装在为您进行协调的现有线程安全数据类型中。我们会谈谈 关于今天。
- 同步。使用同步来防止线程同时访问变量。同步就是你 需要构建自己的线程安全数据类型。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。