람다 Final, Effectively Final

최종윤·2023년 1월 14일

자바

목록 보기
2/6

도입

JLS는 "효과적으로 최종 변수에 대한 제한은 동적으로 변경되는 로컬 변수에 대한 액세스를 금지하며, 이 변수의 캡처는 동시성 문제를 야기할 가능성이 있다"고 말할 때 약간의 힌트를 준다 하지만 그것은 무슨 의미인가요?

다음 섹션에서는 이 제한 사항을 자세히 살펴보고 Java가 이 제한 사항을 도입한 이유를 알아보겠습니다. 단일 스레드 및 동시 응용 프로그램에 어떤 영향을 미치는지 보여주는 예제를 보여주고, 이 제한을 해결하기 위한 일반적인 안티 패턴도 밝혀낼 것입니다.

2. Capturing Labmdas

람다 식은 외부 범위에 정의된 변수를 사용할 수 있습니다. 우리는 이 lambdas를 Capturing lambdas라고 부릅니다. 정적 변수, 인스턴스 변수 및 로컬 변수를 캡처할 수 있지만 로컬 변수만 최종 또는 효과적으로 최종 변수여야 합니다.

초기 버전의 자바에서는 익명의 내부 클래스가 이를 둘러싼 메서드에 로컬 변수를 캡처했을 때 컴파일러가 만족하기 위해 로컬 변수 앞에 최종 키워드를 추가해야 했다.

이제 컴파일러는 최종 키워드가 존재하지 않는 동안 참조가 전혀 변경되지 않는 상황을 인식할 수 있습니다. 이는 사실상 최종 키워드임을 의미합니다. 우리는 컴파일러가 최종이라고 선언해도 불평하지 않는다면 변수는 사실상 최종이라고 말할 수 있다.

3. 람다 포획의 지역 변수

간단히 말해서, 이것은 컴파일되지 않습니다:
Supplier<Integer > incrementer(int start) { return () -> start++; }
start는 로컬 변수이며 람다 식 내부에서 수정하려고 합니다.

이것이 컴파일되지 않는 기본적인 이유는 람다가 start 값을 캡처하고 있기 때문입니다. 즉, start 값의 복사본을 만드는 것 때문입니다. 변수를 final 으로 강제 지정하면 람다 내부에서 start를 증가시키면 실제로 start method parameter 수정될 수 있다는 인상을 주지 않습니다.

????
하지만, 왜 그것은 복사를 만들까요? 자, 우리가 우리의 method 에서 returning lambda 에 주목하세요. 따라서 람다는 start method parameter가 가비지 수집될 때까지 실행되지 않습니다. 이 람다가 이 메서드 밖에서 살기 위해서는 Java가 start 복사본을 만들어야 합니다.

3.1. 동시성 문제

Java가 로컬 변수를 capture한 값에 연결된 상태로 유지하도록 허용했다고 해 보겠습니다.

``
public void localVariableMultithreading() {
boolean run = true;
executor.execute(() -> {
while (run) {
// do operation
}
});

run = false;

``
???
이것은 결백해 보이지만, "가시성"이라는 음흉한 문제를 가지고 있다. 각 스레드는 자체 스택을 가지고 있다는 것을 기억하십시오. 그렇다면 우리의 while loop이 다른 스택에서 run 변수의 변화를 볼 수 있도록 하는 방법은 무엇입니까? 다른 상황에서는 동기화된 블록 또는 volatile 키워드를 사용할 수 있습니다.

그러나 자바는 효과적으로 최종적인 제한을 부과하기 때문에, 우리는 이와 같은 복잡성에 대해 걱정할 필요가 없다.

4. 람다 포획 시 정적 변수 또는 인스턴스 변수

람다 표현식에서 정적 변수 또는 인스턴스 변수를 사용하는 것과 비교하면 앞의 예제는 몇 가지 질문을 제기할 수 있습니다.

시작 변수를 인스턴스 변수로 변환하는 것만으로 첫 번째 예제를 컴파일할 수 있습니다:

``
private int start = 0;

Supplier incrementer() {
return () -> start++;
}
``
그런데 왜 여기서 시작의 가치를 바꿀 수 있을까요?

간단히 말해서, 구성원 변수가 저장되는 위치에 대한 것입니다. 로컬 변수는 스택에 있지만 멤버 변수는 힙에 있습니다. 힙 메모리를 다루고 있기 때문에 컴파일러는 람다가 최신 start 값에 액세스할 수 있음을 보장할 수 있습니다.

두 번째 예도 마찬가지로 수정할 수 있습니다:

``
private volatile boolean run = true;

public void instanceVariableMultithreading() {
executor.execute(() -> {
while (run) {
// do operation
}
});

run = false;

}
``
volatile 키워드를 추가한 이후 다른 스레드에서 실행된 경우에도 실행 변수가 람다에 표시됩니다.

일반적으로 인스턴스 변수를 캡처할 때 마지막 변수를 캡처하는 것으로 생각할 수 있습니다. 어쨌든 컴파일러가 불평하지 않는다고 해서 특히 멀티스레딩 환경에서 예방 조치를 취하지 말아야 하는 것은 아닙니다.

5. 해결책 회피

로컬 변수에 대한 제한을 피하기 위해 누군가 변수 홀더를 사용하여 로컬 변수의 값을 수정하는 것을 생각할 수 있습니다.

배열을 사용하여 단일 스레드 응용 프로그램에 변수를 저장하는 예를 살펴보겠습니다:
``
public int workaroundSingleThread() {
int[] holder = new int[] { 2 };
IntStream sums = IntStream
.of(1, 2, 3)
.map(val -> val + holder[0]);

holder[0] = 0;

return sums.sum();

}
``
스트림이 각 값에 2를 합한다고 생각할 수 있지만 람다를 실행할 때 사용할 수 있는 최신 값이기 때문에 실제로는 0을 합한 것입니다.

한 단계 더 나아가 다른 스레드에서 합계를 실행해 보겠습니다:

``
public void workaroundMultithreading() {
int[] holder = new int[] { 2 };
Runnable runnable = () -> System.out.println(IntStream
.of(1, 2, 3)
.map(val -> val + holder[0])
.sum());

new Thread(runnable).start();

// simulating some processing
try {
    Thread.sleep(new Random().nextInt(3) * 1000L);
} catch (InterruptedException e) {
    throw new RuntimeException(e);
}

holder[0] = 0;

}
``
여기서 우리가 합산하는 가치는 무엇입니까? 시뮬레이션 처리에 걸리는 시간에 따라 다릅니다. 다른 스레드가 실행되기 전에 메소드 실행이 종료될 정도로 짧으면 6이 인쇄되고, 그렇지 않으면 12가 인쇄됩니다.

일반적으로 이러한 종류의 해결 방법은 오류가 발생하기 쉽고 예측할 수 없는 결과를 초래할 수 있으므로 항상 피해야 합니다.

6. 결론

이 기사에서는 람다 표현식이 최종 또는 효과적으로 최종 로컬 변수만 사용할 수 있는 이유에 대해 설명했습니다. 앞에서 살펴본 바와 같이 이러한 제한은 이러한 변수의 다양한 특성과 Java가 이러한 변수를 메모리에 저장하는 방식에서 비롯됩니다. 또한 일반적인 해결 방법을 사용할 때의 위험성도 보여주었습니다.

profile
https://github.com/jyzayu

0개의 댓글