ThreadLocal

appti·2024년 2월 17일
0

분석

목록 보기
1/23

서론

ThreadLocal은 스레드 로컬 변수 저장소를 제공하는 기능입니다.
각 스레드가 독립적으로 값을 가질 수 있습니다.

어떤 식으로 데이터를 관리하기에 ThreadLocal이 각 스레드에 독립적인 저장 공간을 제공하는지 궁금해 get(), set(), remove()의 동작 과정을 분석했습니다.

사전 지식

ThreadLocalMap

ThreadLocal의 내부 클래스입니다.
각 스레드가 고유하게 가지며, 스레드 별로 독립적인 값을 가지는 데 사용됩니다.

Thread의 필드로 가지고 있는 모습을 확인할 수 있습니다.

이름에 Map이 포함되어있지만, 컬렉션의 Map을 구현하지는 않은 클래스입니다.

ThreadLocalMap은 현재 동작중인 스레드를 해싱 처리해 table의 인덱스를 지정하고, Entry[]에서 인덱스에 지정한 요소를 저장합니다.

Entry

ThreadLocalMap에 값을 저장할 때 사용하는 클래스입니다.
ThreadLocalMap 내부 클래스로 정의되어 있습니다.

ThreadLocalMap의 필드인 table로 관리됩니다.

ThreadLocalMap의 생성자에서 기본 크기인 16으로 생성합니다.

Entry는 위에서 선언하는 것처럼 key로는 ThreadLocal을 가지고, value로는 set()으로 지정한 파라미터를 가집니다.

Entry에서 자주 사용하는 메서드 refersTo()는 Reference의 메서드로, 해당 참조의 값이 객체인지 아닌지(= null인지) 확인하는 용도로 사용됩니다.

WeakReference

canonicalizing mappings을 구현하기 위해 사용되는 클래스로, ThreadLocalMap에서 데이터를 관리하는 Entry가 상속하고 있는 클래스입니다.

강한 참조의 반대로, 객체에 대한 참조가 존재하더라도 필요에 의해 GC의 대상이 될 수 있도록 하는 기능입니다.
WeakReference를 상속한 객체는 JVM의 GC가 메모리를 회수할 필요하고, 다른 곳에서 강한 참조가 없다면 GC의 대상이 됩니다.

ThreadLocal에서 데이터를 사용한 이후 clear 하지 않을 때를 대비한 기능이라고 볼 수 있습니다.

TerminatingThreadLocal

특정 TerminatingThreadLocal 인스턴스에 바인딩된 값을 가진 스레드가 종료될 때 호출되는 기능을 가지고 있는, ThreadLocal의 확장입니다.

네이티브 바이트 버퍼, 네이트 버퍼의 스레드 로컬 캐시를 정리하기 위해 JDK 내부에서 사용됩니다.

carrier 스레드에서만 사용된다는 특징을 가지고 있습니다.

get()

다음과 같은 순서로 동작합니다.

  • Thread.currentThread()로 현재 CPU를 할당받은 스레드의 참조값을 찾아옵니다.
  • getMap(t)를 통해 CPU를 할당받은 스레드의 ThreadLocalMap을 가져옵니다.
  • map이 존재한다면 ThreadLocalMap에 해당 스레드가 가지고 있는 Entry를 조회합니다.
  • Entry가 존재한다면 ThreadLocal의 제네릭으로 변환한 뒤 그 값을 반환합니다.
  • map == null || entry == null인 경우 setInitialValue()의 반환값을 반환합니다.

getMap()

Thread.currentThread()의 threadLocals를 반환합니다.

setInitialValue()

다음과 같은 순서로 동작합니다.

  1. initialValue()의 반환 값을 세팅합니다.
  2. Thread.currentThread()의 참조값을 반환합니다.
  3. Thread의 ThreadLocalMap이 존재한다면 기본 값을 세팅합니다.
  4. Thread의 ThreadLocalMap이 존재하지 않는다면 createMap()으로 ThreadLocalMap을 생성합니다.
  5. ThreadLocal이 TerminatingThreadLocal이라면, 스레드가 해제되는 것을 알리기 위해 등록 작업을 수행합니다.
  6. 값을 반환합니다.

initialValue()

null을 반환합니다.

ThreadLocalMap이 존재하지 않거나, 존재하더라도 해당 스레드에서 ThreadLocal에 저장한 데이터가 없을 경우에만 동작하는 메서드입니다.
protected 접근제어자를 통해, ThreadLocal을 커스터마이징할 때 초기값을 자유롭게 세팅할 수 있도록 한 메서드입니다.

createMap()

Thread.currentThread()의 threadLocals를 초기화합니다.

get() 메서드의 동작을 요약하자면 다음과 같습니다.

  1. CPU를 할당받아 동작중인 스레드 조회
  2. 동작중인 스레드의 threadLocals가 존재하며, 해당 스레드에 저장한 값(entry)이 있는 경우 그 값을 제네릭 타입으로 변환해 반환합니다.
  3. threadLocals가 null인 경우 threadLocals를 초기화하고 null을 반환합니다.
  4. threadLocals는 초기화했지만 해당 스레드에 저장한 값(entry)가 없는 경우 null을 반환합니다.

여기서 가장 큰 특징은 get()임에도 불구하고 threadLocals를 초기화하는 로직이 존재한다는 점입니다.

이는 지연 초기화(lazy initialization)로, ThreadLocal에서 get()을 호출했다는 의미는 스레드에서 ThreadLocal을 사용할 것이 분명하기 때문에 set()을 하지 않았더라도 threadLocals를 초기화해주기 위함이라고 생각합니다.

다음으로 set() 메서드를 살펴보도록 하겠습니다.

set()

get()에 비해 간단한 모습을 확인할 수 있습니다.

동작 과정은 다음과 같습니다.

  1. Thread.currentThread()의 참조값을 반환합니다.
  2. getMap(t)를 통해 CPU를 할당받은 스레드의 ThreadLocalMap을 가져옵니다.
  3. map이 존재한다면 ThreadLocalMap에 해당 스레드를 key로, value를 지정한 값으로 지정합니다.
  4. map이 존재하지 않는다면 createMap을 통해 threadLocals를 초기화합니다.(get()과 동일)

remove()

동작 과정은 다음과 같습니다.

  1. Thread.currentThread()의 참조값을 반환합니다.
  2. getMap(t)를 통해 CPU를 할당받은 스레드의 ThreadLocalMap을 가져옵니다.
  3. map이 존재한다면 해당 스레드가 가지고 있는 데이터를 전부 삭제합니다.

전부 삭제하기 위해 사용되는 내부 remove() 메서드의 경우 Entry[]에 저장된 값을 모두 제거하기 위해 ThreadLocalMap.set()과 유사한 과정을 수행하고 있음을 확인할 수 있습니다.

스프링 사용 예시

TransactionSynchronizationManager

스프링에서 트랜잭션을 관리하는 추상 클래스입니다.
ThreadLocal을 많이 사용하는 것에서 알 수 있듯이, 멀티 스레드 환경에서 트랜잭션 리소스를 스각 스레드마다 독립적으로 관리하는 기능을 제공합니다.
여기서 특히 중요한 필드는 resources와 synchronizations 입니다.

resources는 바인딩된 리소스를 관리하는 필드입니다.
key로는 DataSource와 같은 트랜잭션 리소스를 식별할 수 있는 객체를 지정합니다.
value로는 실제 트랜잭션 리소스를 지정합니다.
JDBC의 경우 key = DataSource, value = Connection입니다.

synchronizations는 트랜잭션 동기화 콜백을 관리하는 TransactionSynchronization를 저장하는 필드입니다.
TransactionSynchronization는 트랜잭션 생명 주기 이벤트에 대한 콜백 메서드를 정의해 커밋/롤백될 때 호출할 수 있습니다.
이를 통해 전처리/후처리가 가능합니다.

RequestContextHolder

RequestContextHolder는 스레드에 바인딩된 웹 관련 컨텍스트 정보를 관리하는 기능을 제공합니다.

RequestAttributes를 저장하는 ThreadLocal을 사용합니다.

requestAttributesHolder 필드의 경우 현재 스레드에 바인딩된 RequestAttributes를 저장합니다.
그러므로 현재 웹 관련 컨텍스트 정보를 저장할 수 있습니다.

inheritableRequestAttributesHolder 필드의 경우 부모 스레드에 저장된 웹 관련 컨텍스트 정보를 관리합니다.
부모 스레드에서 웹 관련 컨텍스트 정보가 필요한 경우 사용합니다.

LocaleContextHolder

LocaleContextHolder는 현재 스레드의 Locale 정보를 관리합니다.

RequestContextHolder와 유사한 구조로, localeContextHolder 필드는 현재 스레드의 Locale 정보를 관리하고 inheritableLocaleContextHolder는 부모 스레드의 Locale 정보에 접근할 수 있을 때 사용합니다.

결론

  • ThreadLocal은 Thread.threadLocals라는 필드로 관리됩니다.
  • ThreadLocal에 저장된 값은 ThreadLocalMap.table이라는 배열로 관리됩니다.
    이로 인해 ThreadLocal을 전역변수처럼 사용이 가능합니다.
  • table 배열에서 인덱스를 현재 동작중인 스레드를 해싱 처리한 값으로 구하기 때문에 각 스레드마다 고유하게 값을 관리할 수 있습니다.
  • 지연 초기화를 위해 set() 뿐만 아니라 get()에서도 ThreadLocalMap을 생성합니다.
profile
안녕하세요

0개의 댓글