译自 Why Do Local Variables Used in Lambdas Have to Be Final or Effectively Final? 以学习为目的翻译,适当省略部分内容和增加个人补充
简介 Java8 提供了lambda表达式,同事也给出了实际final变量
的概念,意思是lamdba表达式使用的局部变量必须是显式生命为final,或事实上是final,即声明后不再修改。你有没用想过其中的原因呢?
oracle的官方文档JLS 中15.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) { } }); run = false ; }
那么这段代码将会有潜在的‘可见性’问题。 可以分一下几种可能考虑:
因为每个线程都有各自的栈,这该如何保证while循环每次都能正确看到其他栈中run
变量发生的改变? 答案是,使用用synchronized
或volatile
关键字
多线程的情况下,使用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) { } }); run = false ; }
这里的run
参数加上了volatile
,在别的线程执行的过程中,对于lambda也是可见的。 简单地说,当lambda捕获一个实例变量,我们可以认为它捕获的是final变量this
。
补充: 成员变量(实例变量):存在于对象所在的堆内存,随着对象的建立而建立,随着对象的消失而消失 静态变量:静态变量随着类的加载而存在,随着类的消失而消失,存储在方法区(共享数据区)的静态区