NDM

[Java] lambda에서 final변수만 사용 가능한 이유 - 멀티스레드의 비밀 본문

Java

[Java] lambda에서 final변수만 사용 가능한 이유 - 멀티스레드의 비밀

ndm.jr 2022. 9. 15. 10:45

Java를 사용한 프로젝트를 하고있는데, Stream API와 람다식을 사용하는 경우가 많습니다(Java11 사용중입니다)

 

떄문에 더 잘 알아보고 사용하는 것이 좋다는 취지에서

Stream과 람다식에 대해 조금 더 알아보려고 이번 포스팅을 작성합니다.

 

목차는 다음과 같습니다

  • Final과 Effective Final
  • Lambda에서 Final만 참조가 가능한 이유
  • 동작원리의 관점에서 바라본 Lambda에서 Final만 참조가 가능한 이유
  • 지역변수를 참조하는 lambda와 클래스, 인스턴스 변수를 참조하는 lambda의 차이

Final과 Effective Final

 

Java의 Final 키워드는 모두가 알고 있듯이 변수 앞에 붙이면 상수가 됩니다.

그리고 상수는 반드시 초기화를 해줘야하며, 이후 변경이 불가능하다는 특징을 지니고 있습니다.

 

그렇다면 Effective Final은 무엇일까요? 사전적인 정의는 이렇게 쓰여있습니다.

A variable or parameter whose value is never changed after it is initialized is effectively final.

출처 : https://docs.oracle.com/javase/tutorial/java/javaOO/localclasses.html

 

번역하면 초기화된 이후 한번도 값이 변경되지 않은 변수나 파라미터를 Effectively Final 한 상태라고 합니다.

즉, 일반 변수를 초기화한 뒤 이후 값을 한번도 변경하지 않으면 상수와 동일한 상태로 간주할 수 있다 는 의미이죠

 

그렇다면 왜 이런 상태가 나온걸까요? Multi-Thread환경의 동시성 때문입니다.

 

 int 변수의 값이 10이면 출력문을 찍는 코드입니다. 둘다 람다식에서 외부 지역변수를 사용하고 있음에도 문제가 없어보입니다. 하지만 코드를 이렇게 바꿔보겠습니다.

EffectiveFinal 상태의 변수를 수정했으니 이 변수는 더이상 EffectiveFinal 상태가 아닙니다. 컴파일러에서 에러를 잡아냈고, 그럼 저희는 의문이 생깁니다.

 

람다에서 외부지역변수를 참조할 때 왜 일반 변수는 불가능하고 final만 가능할까?

 

여기서 알고 넘어가야할 점이 있습니다. 람다는 별도의 스레드를 할당받아 명령을 수행한다는 것입니다.

그렇다면 여러 스레드가 각각 람다식을 실행중이라고 했을 때, 람다식 내부의 변수에 영향을 줄 수 있다면 어떻게 될까요?

 

네, 동시성을 보장하지 못합니다. 다른 스레드에서 변수가 바뀌어버렸기 때문이죠

 


동작원리의 관점

 

Java Runtime Data Area의 구조를 도식화한 그림입니다.  Stack과 Heap을 봐주세요

 

저는 위에서

  • 람다는 별도의 스레드로 동작하며
  • 코드 상에서 람다식 바깥에 있는 외부의 지역변수를 참조했습니다.

 

이상하지 않나요??

  • Stack은 Thread마다 개별로 할당되는 공간이고
  • 람다는 별도의 스레드로 동작하며
  • 참조한 외부의 지역변수는 외부 Thread의 Stack에 생성될텐데

어떻게 람다식을 수행한 스레드에서 다른 스레드의 지역변수를 참조할 수 있었을까요?

정답은 람다가 외부지역변수를 참조해 사용할 떄, 이를 자신의 스레드로 복사해온 뒤 사용하기 떄문입니다. 이를 Capturing lambda라고 합니다.

 

여기까지 이해하셨다면 Lambda가 Final 변수만 사용가능한 이유를 동작원리의 관점에서 새롭게 설명이 가능합니다.

자신의 Stack에 변수를 복사해와서 사용해야 하는데, 값 수정이 가능한 일반 변수를 복사해온다고 생각해보세요. 외부와 전혀 Sync가 되지 않아 동시성 문제가 발생합니다.

 


이상하다???

 

 

이 사진을 보고 이상하다라는 느낌이 드셨다면 이번 포스팅을 충분히 이해하셨을거라고 생각합니다.

final 키워드가 붙지 않은 변수에서 값 수정까지 했는데 컴파일러가 에러를 띄우지 않고 있습니다. 왜그럴까요?

 

정답은 참조하고 있는 변수가 지역변수가 아니기 떄문입니다.

 

외부변수 중 지역변수가 아닌 인스턴스 변수와 클래스 변수는 Stack이 아닌 모든 스레드가 공유하는 영역인 Heap에서 관리합니다.(Java8 이후 기준입니다.)

 

떄문에 변수를 람다 스레드로 복사해올 필요도 없으며, 변수가 모든 스레드가 접근 가능한 공간에 위치해 있어 언제나 최신 데이터에 접근하는 것을 보장할 수 있습니다. 단, 멀티스레드 환경에서는 Synchronized나 Volatile 등으로 동기화를 해줄 필요는 있습니다.

 


출처

https://bbbicb.tistory.com/50

 

Java Stream과 Multi Thread

서론 코딩 테스트를 했을 때, Stream을 사용했는데 사용한 이유에 대해서 면접관이 물어본 적이 있다. 나는 대충 "사람의 언어에 가까운 함수형 프로그래밍으로 가독성이 좋다" 이런 식으로 대답

bbbicb.tistory.com

https://vagabond95.me/posts/lambda-with-final/

 

[Java] lambda 와 effectively final - 기록은 기억을 지배한다

자바 8에서 추가된 람다식에는 다음과 같은 규칙이 존재한다. 람다식은 외부 block 에 있는 변수에 접근할 수 있다. 외부에 있는 변수가 지역 변수 일 경우 final 혹은 effectively final 인 경우에만 접

vagabond95.me