前言

使用lambda开发时,在表达式内部获取需要捕获外部变量,同时进行修改,这时idea编译器可能就提示你:Variable used in lambda expression should be final or effectively final.

分析

通过几个例子来分析下:

示例1:lambda表达式内部修改外部变量Integer,编译失败

  @Test
public void test1 () {
    List<Long> list = Arrays.asList(1L, 2L);
    Integer i = 256;
    // 对象唯一标识符
    System.out.println(System.identityHashCode(i)); 
    list.forEach((item) -> {
        // 编译失败
        // i++;

        // 对象唯一标识符,与上面的相对
        System.out.println(System.identityHashCode(i)); 
        System.out.println(item + ":" + i);
    });
}

示例2:将Interger修改为AtomicInteger,内部修改,编译运行正常

@Test
public void test2 () {
    List<Long> list = Arrays.asList(1L, 2L);
    AtomicInteger i = new AtomicInteger();
    list.forEach((item) -> {
        i.getAndIncrement();
        System.out.println(item + ":" + i);
    });
}

示例3:将Interger修改为只有1个元素的数组,内部修改,编译运行正常

@Test
public void test3 () {
    List<Long> list = Arrays.asList(1L, 2L);
    final Integer[] i = {0};
    list.forEach((item) -> {
        i[0] = i[0] + 1;
        System.out.println(item + ":" + i[0]);
    });
}


示例4:将Interger修改为外部变量Integer,编译运行正常

private Integer num = 256;
@Test
public void test4 () {
    List<Long> list = Arrays.asList(1L, 2L);
    list.forEach((item) -> {
        num++;
        System.out.println(item + ":" + num);
    });
}

首先需要理解下:
1、在Java的线程模型中,栈帧中的局部变量是线程私有的,不需要考虑线程安全。
----虚拟机栈结构:
------当前线程:{当前栈帧[局部变量表、操作数栈、动态链接、返回地址等]}{栈帧[…]}{栈帧[…]}…
------线程N:{当前栈帧[局部变量表、操作数栈、动态链接、返回地址等]}{栈帧[…]}{栈帧[…]}…

2、java 是值传递,如果是个对象,传递的是引用地址,可以通过 System.identityHashCode(i);看对象唯一标识符(见示例1)

所以:
线程模型中,每个线程有多个栈帧,每个栈帧有自己的局部变量表,可以理解成示例1中Integer变量,该变量是线程内绝对安全的,在经过lambda表示式后,后续的代码再访问不应该有线程安全问题,不然就违背了线程模型,那么经过lambda表示式传递给匿名内部类后,它是将地址传递了出去,一旦地址值在另一个线程中改变,这时就会导致程序结果混乱,出现线程安全问题,故而直接简单点不让修改引用值。

结论

1、线程局部变量泄漏出去,无法保障安全,所以需要final,不能修改"值"(见示例1)

2、AtomicInteger/数组 可以当作int的容器。因为它是在堆上被分配的,我们完全没有改变这个局部变量的指向(effectively final成立)(见示例2,示例3)

3、非线程局部变量传递给lambda(匿名内部类),属线程共享变量,需要自行保证线程安全(见示例4)

总结

lambda(匿名内部类)允许访问线程域内外部变量,如果还允许修改,会导致线程内的私有变量存在不安全的行为,颠覆java线程模型,所以java语言规范给限制了。