Language/Java

Java - 람다(Lambda)의 멀티 쓰레드(multi-thread) [외부 변수를 final만 허용하는 이유]

JaeHoney 2022. 3. 9. 16:38

람다와 final ?

아래의 코드는 람다식을 사용한 코드입니다. 

public static void main(String[] args) {
    int num = 10;
    Runnable printNum = () -> System.out.println(num);
}

위코드는 문제 없이 실행됩니다. 그런데 아래 코드를 보시면 num++; 한 줄 추가했을 뿐인데, 컴파일 에러가 납니다. 

에러가 나는 이유는 Java8 람다에는 외부 지역 변수가 final이어야 한다는 조건이 있기 때문입니다.

 

Q. 엥? 그런데 첫 번째 코드에서는 final이 없는데 에러가 왜 안났지?

 

-> 이유는 num변수는 현재 effectively final이기 때문입니다. 변수가 초기화되고 값의 변화가 한 번도 없다면, 컴파일러에서는 해당 변수를 final로 취급합니다. 그 변수를 effectively final이라고 할 수 있습니다

 

-> 두 번째 코드에서는 num을 변경하는 코드를 추가해서 컴파일러에서는 effectively final이라고 취급하지 않은거죠.

람다 캡쳐링(Lambda Capturing)

람다식은 여러 개의 쓰레드에서 외부 변수를 참조할 수 있습니다. 그래서 해당 변수를 가져올 때 final 변수 복제본을 스택에 생성해서 사용하고 block이 끝나면 소멸시킵니다.

 

[그러면 Why! 람다식에서는 외부 참조변수가 final이 아니면 안될까요?]

람다는 별도의 쓰레드를 활용하여 병렬 처리를 수행할 수 있습니다.(ex. parallelStream) 그래서 멀티쓰레드 환경에서, 람다식이 먼저 실행될 지, 외부 참조 변수가 변경되는 부분이 먼저 실행될 지는 알 수 없습니다.

 

그래서 람다식 안에서는 외부 변수를 변경할 수는 없게 했으며, 마치 변경이 가능한 것처럼 착각을 일으킬 수 있기에 컴파일 에러가 발생하도록 처리한 것입니다.

 

즉, 아래 두 줄의 코드가 각각 다른 Thread에서 실행된다면, Thread1의 lambdaPrint(name)이 실행되는 도중에, Thread2의 name.replace가 실행되고 먼저 완료되서 Thread1의 lambdaPrint의 결과값이 달라질 수도 있습니다. 

lambdaPrint(name); // Thread 1
name.replace("Lee", "Kim"); // Thread 2

Thread safety 하지 않다는 거죠. 그러면 예측 결과를 알 수 없게 됩니다.

 

그래서 외부 참조 변수가 변경되지 못하게 final을 사용합니다.

 

-> 이 부분은 외부 상태를 참조해서 변경하지 않겠다는 순수 함수(함수형 프로그래밍)의 의미와 일맥상통합니다.

 

AtomicVariable도 람다 내부에서 사용이 가능한데, 이는 AtomicVariable 원자적이기 때문은 아닙니다.

단지 인스턴스이기 떄문입니다. 초기화를 하는 것이 아니라 내부 상태를 변경하는 것은 가능합니다. AtomicVariable의 경우 원자적이기 때문에 안전하게 사용할 수 있는 것뿐이죠.