【译】为何Lambda中的局部变量必须是final

译自 Why Do Local Variables Used in Lambdas Have to Be Final or Effectively Final?
以学习为目的翻译,适当省略部分内容和增加个人补充

简介

Java8 提供了lambda表达式,同事也给出了实际final变量的概念,意思是lamdba表达式使用的局部变量必须是显式生命为final,或事实上是final,即声明后不再修改。你有没用想过其中的原因呢?

oracle的官方文档JLS15.27.2. Lambda Body一节有提到,“禁止使用可动态修改的局部变量,因为有可能导致并发问题”,这是什么意思呢?

接下来,我们会深入了解这个限制使用可变局部变量的规定,并给出相关的例子证明它是如何影响单线程和并发程序的。我们也会展示一个与这个限制相关常用的反范式例子。

2. 捕获Lambdas

Lambda表达式可以使用内层和外层作用域中敌营的变量,我们称之为捕获Lambdas,包括静态变量、实例变量和局部变量,但局部变量必须是final事实上是final

在早期Java版本中,我们需要在匿名内部类使用的外部变量前加上final关键字。
现在Java语法糖会自动帮我们识别这种情况并在编译之前帮我们加上遗漏的final关键字,因此,代码中即使没有显示声明变量是final也没关系,但这并不会改变变量是final的事实(如果你尝试修改变量,则会出现编译错误)。

3.捕获lambdas中的局部变量

1
2
3
Supplier<Integer> incrementer(int start) {
return () -> start++;
}

上述代码中局部变量start在lambda表达式中被修改,无法编译。
这段代码无法编译的原因是实际上lambda捕获的是start的值,意思就是start的一个拷贝副本。这就要求start变量必须是final的,这样才能避免lamdba中的start++操作改变了incrementer方法的参数值。

但为什么要拷贝呢?请注意一点,这个方法return的是一个lambda,因此,这个incrementer方法执行完了,lambda不会都执行,此时incrementer方法的局部变量start(在栈中)已经被垃圾回收,所以Java会为start参数做一个拷贝副本,实际访问的也是这个副本,而不是原始变量。只有这样,return的lambda才能在incrementer方法之外‘存活’。

补充:
final可修饰引用数据类型基本数据类型

4.并发问题

举个反例,如果lambda捕获的局部变量可以被修改(不是final,不拷贝)。

1
2
3
4
5
6
7
8
9
10
public void localVariableMultithreading() {
boolean run = true;
executor.execute(() -> {
while (run) {
// do operation
}
});

run = false;
}

那么这段代码将会有潜在的‘可见性’问题。
可以分一下几种可能考虑:

  1. 因为每个线程都有各自的栈,这该如何保证while循环每次都能正确看到其他栈中run变量发生的改变?
    答案是,使用用synchronizedvolatile关键字
  2. 多线程的情况下,使用lambda的线程,可能会在分配该局部变量的的线程将这个变量回收之后,去访问该变量

正是因为有了这个强制final的措施,我们才不需要亲自考虑这几种情况。

补充:
Java的不可变类(Immutable Objects)有一个特点就是线程安全。在多线程情况下,一个可变对象的值很可能被其他进程改变,这样会造成不可预期的结果,而使用不可变对象就可以避免这种情况同时省去了同步加锁等过程,因此不可变类是线程安全的

5.静态变量和实例变量

第一个例子,把start变量改为实例变量,即可成功编译。

1
2
3
4
5
private int start = 0;

Supplier<Integer> incrementer() {
return () -> start++;
}

为什么实例变量start可以被修改呢?
这和成员变量存储的位置有关。局部变量存储在栈中,成员变量存在于堆中。因为我们一直在操作堆内存,所以编译器可以保证变量start始终是正确的。

第二个例子,也可修改为

1
2
3
4
5
6
7
8
9
10
11
private volatile boolean run = true;

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

run = false;
}

这里的run参数加上了volatile,在别的线程执行的过程中,对于lambda也是可见的。
简单地说,当lambda捕获一个实例变量,我们可以认为它捕获的是final变量this

补充:
成员变量(实例变量):存在于对象所在的堆内存,随着对象的建立而建立,随着对象的消失而消失
静态变量:静态变量随着类的加载而存在,随着类的消失而消失,存储在方法区(共享数据区)的静态区

文章作者: xs
文章链接: xsinx.com/2019/06/16/为何Lambda中的局部变量必须是final/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Xsinx