简介
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
3Supplier (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,不拷贝)。
public void localVariableMultithreading(){
boolean run = true;
executor.execute(() -> {
while (run) {
}
});
run = false;
}
那么这段代码将会有潜在的‘可见性’问题。
可以分一下几种可能考虑:因为每个线程都有各自的栈,这该如何保证while循环每次都能正确看到其他栈中run变量发生的改变?
答案是,使用用synchronized或volatile关键字
多线程的情况下,使用lambda的线程,可能会在分配该局部变量的的线程将这个变量回收之后,去访问该变量
正是因为有了这个强制final的措施,我们才不需要亲自考虑这几种情况。
补充:
Java的不可变类(Immutable Objects)有一个特点就是线程安全。在多线程情况下,一个可变对象的值很可能被其他进程改变,这样会造成不可预期的结果,而使用不可变对象就可以避免这种情况同时省去了同步加锁等过程,因此不可变类是线程安全的
5.静态变量和实例变量
第一个例子,把start变量改为实例变量,即可成功编译。
private int start = 0;
Supplier (){
return () -> start++;
}
为什么实例变量start可以被修改呢?
这和成员变量存储的位置有关。局部变量存储在栈中,成员变量存在于堆中。因为我们一直在操作堆内存,所以编译器可以保证变量start始终是正确的。
第二个例子,也可修改为
private volatile boolean run = true;
public void instanceVariableMultithreading(){
executor.execute(() -> {
while (run) {
}
});
run = false;
}
这里的run参数加上了volatile,在别的线程执行的过程中,对于lambda也是可见的。
简单地说,当lambda捕获一个实例变量,我们可以认为它捕获的是final变量this。
补充:
成员变量(实例变量):存在于对象所在的堆内存,随着对象的建立而建立,随着对象的消失而消失
静态变量:静态变量随着类的加载而存在,随着类的消失而消失,存储在方法区(共享数据区)的静态区