티스토리 뷰
개발을 하다보면 thread-safe 하지 않은 클래스들을 많이 마주하게 되고 그러한 클래스들을 사용할 때 ThreadLocal을 사용해서 thread-safe하게 만들어주곤 했다. 하지만 단순히 ThreadLocal을 사용하면 변수를 thread-safe하게 만들어준다라는 개념만 있었지 thread-safe가 정확히 무엇을 의미하는지, thread-safe하지 않을 경우 무슨 일이 발생하는지, ThreadLocal은 정확히 무엇이고 어떻게 동작하는지는 알지 못해 이렇게 글을 정리해본다.
thread-safe란?
우선 ThreadLocal이 뭔지 이해하려면 thread-safe가 무엇인지 이해해야 한다.
가끔 라이브러리에서 가져와 사용하는 클래스들을 보면 주석으로 아래와 같이 thread-safe가 언급되는 것을 볼 수 있다. (아래는 java.time.format.DateTimeFormatter의 주석 일부)
...
* it is immutable and is thread-safe.
...
여기서 thread-safe란 멀티 스레드 환경에서 즉 여러 곳에서 동시에 함수나 변수 등에 접근 가능할 때 서로 다른 스레드들이 동시에 같은 것에 접근해서 이용해도 문제가 생기지 않는 다는 것을 의미한다.
= 서로 다른 곳에서 동시에 접근해 작업을 수행해도 결과에는 문제가 없다는 것을 의미한다.
여기서 멀티 쓰레드는 자바에서 보통 Thread, Runable을 이용해서 구현을 하곤한다. 하지만 그러한 쓰레드를 직접적으로 생성해서 발생하는 상황 말고도 특정 클래스의 빈을 생성해놓고 그 빈을 여러 곳에서 동시다발적으로 사용할 때 또한 이러한 동시성을 고려해 개발하는 것이 좋다. (특히 여러 곳에 호출되서 바뀔 수 있는 값의 경우!)
ThreadLocal이란?
그렇다면 ThreadLocal이란 무엇일까. 요즘은 많은 클래스들이 thread-safe하게 만들어지는 것 같지만 thread-safe 하지 않은 클래스 또한 존재한다. 멀티 쓰레드 환경이 아니고 순차적으로 해당 클래스를 사용해서 해결되는 상황이면 굳이 고려하지 않아도 된다.
그 때 ThreadLocal이 사용되곤 하는데 ThreadLocal은 정확히 이야기하면 여러 스레드에서 동시적으로 특정 변수에 접근해서 사용될 때 해당 변수가 다른 스레드에 의해서 변경되어 결과 값이 다르게 나오는 것을 방지하기 위해 사용된다.
ThreadLocal : 스레드(Thread)마다의 고유한 지역(Local) 변수를 넣어서 사용하는 클래스
ThreadLocal은 아래와 같이 thread-safe하게 사용할 변수를 감싸는 형태로 사용된다.
// 1. init 할 때 값 넣기
ThreadLocal<Box> box = ThreadLocal.withInitial(() -> new Box());
// 2. set을 통해 값 넣기
ThreadLocal threadLocal = new ThreadLocal();
threadLocal.set(object);
ThreadLocal에 저장된 값은 각각의 스레드에서 독립적으로 복사본 변수를 갖게 된다. 이 고유한 복사본 변수를 통해 각 스레드들은 각자의 변수에 접근하게 되고 만일 A 스레드가 변수를 변경했을 때 이는 B 스레드에 영향을 주지 않는다.
ThreadLocal 동작 방식
ThreadLocal의 get, set을 보면 ThreadLocal이 어떻게 동작하는지 알 수 있다.
스레드마다의 고유한 변수를 사용하도록 해당 변수를 저장하는 Thread.set()은 아래와 같다. (비슷한 맥락으로 처음에 초기화 하는 ThreadLocal.withInitial()또한 비슷하다.)
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
코드를 보면 Thread에서 ThreadLocalMap이란 것을 꺼내서 그 안에서 저장해놓은 변수를 꺼내는 식으로 처리가 되어 있다.
여기서 Thread의 ThreadLocalMap은 해당 스레드가 갖고 있는 map이다. ThreadLocal에서 지금 현재 스레드의 table에서 현재 ThreadLocal 클래스를 키 값으로 map에 값을 저장하는 것이다.
이렇게 ThreadLocal은 해당 스레드의 map에 변수를 초기화해서 저장해서 해당 변수를 해당 스레드에서만 고유하게 사용하도록 한다. 이 값을 꺼내 사용하는 get 코드는 아래와 같다.
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
get에서 또한 set과 마찬가지로 해당 스레드의 ThreadLocalMap을 가져온다. 그리고 map에서 값을 꺼내 그 값을 반환해야하는 클래스로 형변환을 해서 반환한다.
즉, 정리를 해보면
1. ThreadLocal.set()을 통해 변수 값을 해당 스레드가 가지고 있는 Map에 생성한 ThreadLocal 클래스를 키 값으로 저장한다.
2. ThreadLocal.get()을 통해 변수 값을 해당 스레드가 가지고 있는 Map에서 현재 ThreadLocal 클래스를 키 값으로 가져온다.
주의할 점
ThreadLocal은 위와 같이 해당 스레드에서만 고유하게 사용해야하는 변수를 저장하고 꺼내서 사용하기 좋다.
하지만 만일 스레드를 Thread Pool과 같은 곳에서 꺼내서 사용 후 다시 스레드를 반납하는 식으로 사용할 경우 어떻게 될까? 그렇게 될 경우 Thread Pool에 있는 스레드 중 전에 사용된 스레드의 ThreadLocal로 저장된 값은 전에 사용했던 그대로 값이 있어 문제가 될 수 있다.
이럴 때는 스레드를 Thread Pool에서 꺼낼 때 무조건 ThreadLocal에 저장한 값을 다 초기화 해주거나 사용한 스레드의 ThreadLocal의 값을 아래 코드를 이용해 지워줘야 한다.
threadLocal.remove();
✋ ThreadLocal
https://docs.oracle.com/javase/7/docs/api/java/lang/ThreadLocal.html
✋ ThreadLocal.Entry
'Java' 카테고리의 다른 글
null 처리를 도와주는 Optional<T> (0) | 2022.01.14 |
---|---|
Kryo serializer (0) | 2022.01.03 |
ConstraintValidator로 컨트롤러에서 받는 값 유효성 체크하기 (0) | 2021.10.14 |
for문 보다 Stream API가 느리지만 사용하는 이유 (0) | 2021.10.13 |
Checked Exception vs UnChecked Exception (0) | 2021.10.12 |
- Total
- Today
- Yesterday
- 어노테이션
- HttpClient
- PostgreSQL
- Spring
- JPA
- k8s
- rabbitmq
- Java
- enum
- Intellij
- Caching
- annotation
- 캐시
- 스프링부트
- eclipse
- 데이터베이스
- 자바
- 역직렬화
- mockito
- ssh
- 캐싱
- cache
- springboot
- DB
- 공간데이터
- postgres
- 이클립스
- MAC
- DATABASE
- 메시지큐
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |