- 람다의 특징
- 용어 정리
- 왜 final 또는 effectivly final만 접근할 수 있는가?
들어가기 앞서.
결론 : 멀티쓰레드 환경에서 동작한다면, 람다 내의 각 루프가 다른 쓰레드에서 서로 참조할 수 있기 때문에
레이스 컨디션이 일어날 수 있다.
그러므로 람다식 안에서 람다식 밖의 변수를 참조하려면 final 또는 effectively final 이여야 한다.
하지만 예외로 람다식에서 인덱스
라던가 final이 아닌 지역변수를 참조, 수정하고 싶다면,
Java에서 멀티스레드환경에서 동시성을 보장해주는 Atomic 클래스를 사용하면 된다.
1. Lambda의 특징
람다에서 지역변수를 사용하려면 다음과 두가지 특징이 있다
- 람다표현식에서 사용되는 외부 지역 변수는 복사본(캡쳐)이다
- 람다표현식 내부(scope)에서 사용되는 변수가 람다식 바깥에 있는 외부 변수인 경우 (지역변수)
final
혹은effectively fianl
이여야만 한다.- 지역변수가 final 변수가 아니고, 재할당을 하면 람다표현식 내에서는 사용할 수 없다.
지역변수는 명시적으로 final로 선언되어 있어야 한다
- 지역변수가 final 변수가 아니고, 재할당을 하지 않는다면 람다표현식 내에서는 사용할 수 있다.
지역변수는 effectivly final variable 이여야 한다.
- 지역변수가 final 변수가 아니고, 재할당을 하면 람다표현식 내에서는 사용할 수 없다.
- effective final : 메서드 안에 정의 된 필드 값(지역변수) , final이 선언되지 않았지만 사실상 변수를 변경하는 부분이 없는 변수
- 메서드가 종료될 때 까지 변수를 변경하지 않으므로 final처럼 동작할 수 있다
- 다음 코드는 final 지역변수와 effectivly final 지역변수 예제이다
// final ex
public void ballCount(Numbers answer) {
final int strike = 0; // final로 명시적으로 선언. final 이므로 람다 내부에서 변경할 수 없다
answer.run(() -> {
// strike = 1; // 에러. final 이므로 람다 내부에서 변경할 수 없다
System.out.println(strike); // 변경하지 않았으므로 정상 동작
});
}
// effectivly final ex
public void ballCount2(Numbers answer) {
int strike = 0; // final로 선언하지 않음. method end시 까지 수정하지 않았으므로 final처럼 동작할 수 있다.
answer.run(() -> {
System.out.println(strike);
});
// method end
}
// effectivly final ex2
public void ballCount2(Numbers answer) {
int strike = 0; // final로 선언하지 않음.
answer.run(() -> {
System.out.println(strike); // 컴파일 에러.
});
strike +=1; // 이 코드때문에 에러. 람다 실행 후 strike 값을 바꿨으므로 effectivly final이 아니다.
}
2. 용어 - 자유변수와 람다 캡처링
class BallGame {
private int tryCount; // 인스턴스 변수
private BallCount ballCount(Numbers answer, Numbers inputNumbers) { // 파라미터도 지역변수
int strike = 0; // 지역변수 , 자유변수
int ball = 0; //지역변수, 자유변수
answer.indexedForEach((a, i) -> { // 람다 바디
inputNumbers.indexedForEach((n, j) -> { // 람다 바디
if (!a.equals(n)) return;
if (i.equals(j)) strike += 1;
else ball += 1;
});
});
return new BallCount(strike, ball);
}
}
- 람다는 인스턴스 변수와 정적 변수를 자신의 바디
{}
에서 사용할 수 있도록 자유롭게 캡처한다 - 자유변수 : 람다 기준에서, 람다 시그니처의 파라미터로 넘겨진 변수가 아니라 외부에서 정의된 변수 (지역변수)
- 위 메서드에서는 int strike, ball이 자유 변수가 된다
- 람다 캡처링 : 람다 바디에서 자유 변수를 참조하는 행위
람다 캡처링을 하는 이유
위 코드의 실행 흐름 상 increment 메서드를 호출한
스레드의 스택 영역에 num이라는 지역 변수가 생성되지만,
람다식을 리턴한 후에는 메서드가 종료되었으니 스택 영역에서 지역변수 num이 사라진다.
반환된 람다표현식은 현재 sum()을 호출하고 있는 스레드가 아닌, 다른 어떤 스레드에서 호출될지 알 수 없다.
num이 사라졌을 뿐더러, 다른 스레드의 스택 영역에 있으므로 접근도 할 수 없다.
그래서 자바는 이 문제를 해결하고자 자유 변수의 복사본
을 만들어 접근을 허용하도록했는데 이걸 람다 캡쳐링이라고 한다.
3. 왜 final 또는 effectivly final만 접근할 수 있는가?
다음 예제를 보자.
increment 메서드는 외부로부터 값을 전달받아서,
람다 표현식을 이용하여 1만큼 증가하는 함수를 전달해주는 메서드이다.
class Example {
public void sum() {
int num = 10; // 지역변수
int incrementNum = this.increment(num).get();
}
private Supplier<Integer> increment(int num) { // 지역변수. 람다 기준으로는 외부에있는 자유변수이다
return () -> num += 1; // 컴파일 에러
}
}
num +=1 부터 컴파일 에러가 난다.
람다표현식 기준으로 외부에 있는 자유변수를 수정하려고 했기 때문이다.
JVM에서는 Heap과 Method 영역을 제외하고는 다른 영역은 쓰레드마다 생성해서 사용한다.
즉 쓰레드마다 별도의 스택 영역이 생성된다.
JVM에서 지역 변수는 스택이라는 영역에 생성되는데,
따라서 지역변수는 쓰레드끼리 공유가 안된다.
하지만 자유 변수는 람다 캡처링에 의해 복사되기 때문에,
다른 스레드에서 실행하는 람다에 의해서 참조할 수 있고,
람다 캡처링에 의해서 복사된 참조 값
을 변경하는 참조하는 람다 코드는,
이 람다 코드가 실행되는 시점에 따라 복사된 참조 값이 어떤 값인지 예측할 수 없기 때문에
(우리는 num 값을 10을 기대했지만, 11로 변해있는지 알 수도 없기 때문, 동기화가 안맞을 수도 있고)
람다내에서 외부 자유변수를 참조할때는 final 또는 effecively final만 참조할 수 있다. (값이 변하지 않는다는걸 증명해줌)
그러니까
람다는 별도의 쓰레드에서 실행이 가능하다.
따라서 원래 지역 변수를 참조하는 메서드를 실행하는 쓰레드를 쓰레드 1이라고 하고
다른 람다가 실행 중인 쓰레드를 쓰레드2라고 하자
쓰레드1은 사라져서 해당 지역변수가 사라졌는데도 불구하고,
람다가 실행 중인 쓰레드는 살아있을 가능성이 있다.
쓰레드2의 람다에서 사라진 쓰레드1의 지역변수를 참조하고 있으면 어떻게 될까?
오류가 날 것 같지만 우리의 예상과는 달리 오류는 나지 않는다.
별도의 쓰레드에서 실행된다면 별도의 스택 영역을 가질테고,
그럼 다른 쓰레드의 스택에 있는 지역변수는 참조조차 할 수 없을탠데
왜 오류는 나지 않고, 어떻게 다른 쓰레드의 스택 영역에 있는 지역 변수를 참조할 수 있는 걸까?
왜냐하면,
람다에서 지역 변수(해당 쓰레드1의 스택)에 직접적으로 접근하는 게 아니라 그 지역변수를 자신(쓰레드2)의 스택에 복사하기 때문이다.
-> 람다 캡처링
그렇기 때문에 별도의 쓰레드(쓰레드2)의 스택에 있는 지역 변수와 동일한 값을 참조할 수 있고,
쓰레드1이 사라져도 쓰레드2에서 수행할 수 있는것이다.
하지만 쓰레드1의 변수를 복사해서 쓰는데 그 변수의 값이 중구난방으로 변경된다고 하면 해당 복사본을 믿고 쓸 수 있을까?
따라서 지역 변수에는 final이어야하거나 final 같이 동작해야한다는 제약 조건이 생긴 것이다.
즉 멀티스레드 환경에서의 동시성 문제를 방지하기 위해서이다.
왜 지역변수만에 제약이 있나?
- 인스턴스 변수는 힙(heap) 에 저장되지만 지역 변수는 메서드 안에 존재하므로 스택(stack)에 위치한다
- 자바에서 메서드는 스택에 위치한다.
- 람다에서 지역 변수에 바로 접근할 수 있다는 가정 하에 람다가 스레드에서 실행된다면 변수를 할당한 스레드가 사라져서! 변수 할당이 해제되었는데도 해당 변수에 접근하려 할 수 없음.
- 따라서 자바 구현에서는 원래 변수에 접근을 허용하는 것이 아니라 자유 지역변수의 복사본을 제공
복사본의 값이 바뀌지 말아야 하기 때문
인스턴스 변수란?
- 클래스 내에 선언된 변수
- 객체 생성 시마다 매번 새로운 변수가 생성됨
- 클래스 변수와 달리 공유되지 않음
인스턴스 변수는 왜 이런 제약이 없는 걸까?
인스턴스 변수는 힙에 존재하고,
쓰레드끼리 공유도 가능하기 때문에 별도로 복사할 필요도 없고,
직접 힙에 접근해서 사용하면 되기 때문이다.
- 인스턴스 변수 캡처는 final 지역 변수
this
를 캡처하는 것과 마찬가지다.
값이 스택에 있는 메서드 지역변수와는 달리,
메모리에서 바로 회수되지 않기 때문에 람다 실행시 값이 존재하는것을 보장할 수 있다.
다만 멀티 스레드 환경에서는 sync를 맞춰주는 작업이 필요하다.
'Java > Java' 카테고리의 다른 글
Java - 동일성과 동등성 ( ==, equals() ) (0) | 2022.11.19 |
---|---|
Java 박싱과 언박싱, 오토박싱 그리고 성능상 주의할점 (0) | 2022.11.13 |
Java String Builder와 StringBuffer의 차이점 (0) | 2022.10.21 |
Java String Pool (String Constant Pool) + String.intern() (0) | 2022.10.21 |
# Java 그룹화, 그룹화 하고 정렬. Stream groupby, groupingBy, sorting Lists after groupingBy (0) | 2022.09.07 |