简介
lambda内使用的外层局部变量必须是final探究
结论
外部局部变量进入lamdba内部,lamdba内部是无法感知到这个变量在外部的变化的,所以为了解决数据不同步的问题,就会要求是final。
解释
为什么你在外层代码修改了变量,lambda内部会感知不到呢?我们知道lamdba的本质是匿名内部类,是函数式接口的实现的实例,请看下面的代码:
1 | public static void test02() { |
我们在使用过程中最直观的感受就是食用lamdba后代码量减少了,匿名内部类拿到外部参数其实也是通过构造器赋值的,lamdba表达式在编译阶段,编译器回创建一个新的class文件,比如Main$1.class。这些文件就是lamdba实例化的类的字节码文件,这个类会有默认的构造器,传入的参数就是我们外部使用的局部变量,所以外层局部变量就这样供内部使用了。
那么请再看下面这段代码:
1 | public static void test02() { |
你认为输出应该是什么?显然我们在输出前已经进行线程启动,但是结果是:[hello out]
那么我们等待一下线程执行完毕再输出呢?
1 | public static void test02() throws InterruptedException { |
结果是:[hello out, hello in]
,显然“hello out”是先进入的,这符合我们的预期,那么我们在线程中先删除“hello out”再添加“hello in”呢?
1 | public static void test02() throws InterruptedException { |
结果是:[hello in]
。那么就有个问题:ArrayList是非同步的,也没有使用final修饰,为什么可以在lamdba中使用?而使用Integer的时候,就必须使用AtomicReference<Integer>
来保证同步呢?
其实是这样的:
因为实例变量存在堆中,而局部变量是在栈上分配,Lambda 表达(匿名类) 会在另一个线程中执行。如果在线程中要直接访问一个局部变量,可能线程执行时该局部变量已经被销毁了,而 final 类型的局部变量在 Lambda 表达式(匿名类) 中其实是局部变量的一个拷贝。这其实就是值传递和应用传递的区别,我们定义的ArrayList在堆中,使用的时候是通过引用实现访问的。
那么就会有一个问题:当多线程顺序执行的时候,不会出问题,并行执行的时候,一定会出问题。如下面的例子:
1 | public static void test02() throws InterruptedException { |
其中第一种方式就是并行计算,输出为:9999737
,而且线程池用的ForkJoinPool,当超过阻塞队列的时候会进入Main线程执行。
而第二种方式,是顺序执行的,所以不会出错,输出为10000000
。
但是其实上述代码是有问题的,因为代码中是先get后set的,所以这并不是一个原子操作,即便使用线程安全的Vector,在并行的时候仍然会出问题,所以我们要配合AtomicReference
使用。
- 本文作者: October
- 本文链接: http://www.octber.xyz/2020/10/13/lambda内使用的外层局部变量必须是final探究/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!