여러 스레드가 같은 인스턴스의 필드에 접근하면, 처음 스레드가 보관한 데이터가 사라질 수 있다. 또한 동시성 문제가 발생한다. 이때, 스레드 로컬을 사용하여 해결할 수 있다.
📚 동시성 문제
같은 인스턴스 필드에 두개 이상의 스레드가 접근해서 값을 변경할 때, 저장한 값과 조회 결과값이 달라지는 현상을 의미한다. 지역 변수가 아닌 객체가 하나밖에 없는 싱글톤이나static
전역 변수에서 발생한다.
해당 스레드만 접근할 수 있는 특별한 저장소를 말한다.
스레드 로컬은 각 스레드마다 별도의 내부 저장소를 제공하고 관리하기 때문에, 동시에 같은 인스턴스의 스레드 로컬 필드에 접근해도 문제가 없다.
쉽게 말해서 창구를 예시로 들자면, 여러 사람이 한 공간에 물건을 보관해도 창구 직원이 사용자별로 물건을 구별해주는 것이다.
자바는 언어차원에서 쓰레드 로컬을 지원하기 위한 java.lang.ThreadLocal
클래스를 제공하고 있다.
자바 스레드 로컬은 해시맵(HashMap
) 방식으로 동작한다.
각 스레드는 ThreadLocal
의 내부 클래스인 ThreadLocalMap
객체를 참조 필드로 가지고 있기 때문에 해당 필드에 새로 생성한 맵을 저장해준다. 쉽게 말해서 스레드마다 자신만의 저장소가 생기는 것이다.
public class Thread implements Runnable {
...
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}
ThreadLocalMap
생성자를 보면 알 수 있듯이, 각 원소의 타입은 Entry
이다. 즉 내부적으로는 Entry
배열(해시 테이블)로 구현이 되어있다.
🔖 Entry?
Entry
는ThreadLocal
의 내부 정적 클래스로value
에는 해당 동시성 문제를 해결하고자 하는, 즉 저장하고자 하는 필드의 값이 들어간다.
생성자에서는 해시코드 값을 계산하여 해당 자리에 초기화 할 첫번째 값을 넣어주고 있는데, 해시코드는 현재 ThreadLocal
객체의 해시코드 값을 활용해서 생성하고 있다.
만약 아래와 같이 여러개의 ThreadLocal
필드가 존재한다 가정해보자. 각 스레드 별로 스레드에 이미 생성되어 있는 ThreadLocalMap
을 가져와서 저장하기 때문에 각 ThreadLocal
의 해시코드 값으로 구분이 필요한 것이다.
private final ThreadLocal<String> nameStore = new ThreadLocal<>();
private final ThreadLocal<Integer> money = new ThreadLocal<>();
현재 스레드 정보를 가져와서, 내부에 ThreadLocalMap
이 없다면 새로 생성한 이후에 자기 자신을 key
로, 저장하고자 하는 값을 value
로 Map에 추가한다.
값을 가져올때도 마찬가지로 현재 스레드 정보를 가져와서 내부에서 ThreadLocalMap
을 꺼내고, 현재 ThreadLocal
객체의 해시값을 기반으로 인덱스를 찾아내서 Entry
배열에서 값을 가져온다.
@Slf4j
public class ThreadLocalService {
// private String nameStore; 동시성 문제가 발생하는 코드
private final ThreadLocal<String> nameStore = new ThreadLocal<>();
public String logic(String name) {
log.info("저장 name={} -> nameStore={}", name, nameStore.get());
nameStore.set(name);
sleep(1000);
log.info("조회 nameStore={}", nameStore.get());
return nameStore.get();
}
...
}
ThreadLocal.set(xxx)
ThreadLocal.get()
ThreadLocal.remove()
이전과 다르게 각자 별도의 저장소를 가지므로 userA
의 조회값이 잘 나오는 것을 볼 수 있다.
스레드 로컬의 값은 사용 후에 꼭 제거를 해주어야 한다.
그렇지 않으면 보통의 WAS
(톰캣) 는 스레드풀로 미리 스레드들을 생성해놓아서 관리하는데, 이때 문제가 발생하게 된다. 아래의 예제를 보자.
A
가 HTTP 저장 요청을 보내고, thread-A
스레드를 할당받아서 스레드 로컬에(전용 보관소)에 사용자 A
의 데이터를 저장한다.WAS
는 사용이 끝난 thread-A
를 제거하지 않고 스레드 풀에 다시 반납한다. 따라서 thread-A
와 함께 스레드 로컬의 데이터도 살아있게 된다.B
가 HTTP 조회 요청을 보내고, thread-A
스레드를 할당받게 된다.thread-A
는 쓰레드 로컬에서 데이터를 조회하는데 사용자 A
의 데이터가 저장되어 있으므로 사용자 B의 요청이지만 A
값을 반환하는 문제가 발생한다.따라서 요청이 끝날 때 쓰레드 로컬의 값을 ThreadLocal.remove()
를 통해서 꼭 제거해 주어야 한다.