여러 스레드가 같은 인스턴스의 필드에 접근하면, 처음 스레드가 보관한 데이터가 사라질 수 있다. 또한 동시성 문제가 발생한다. 이때, 스레드 로컬을 사용하여 해결할 수 있다.
📚 동시성 문제
같은 인스턴스 필드에 두개 이상의 스레드가 접근해서 값을 변경할 때, 저장한 값과 조회 결과값이 달라지는 현상을 의미한다. 지역 변수가 아닌 객체가 하나밖에 없는 싱글톤이나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
이 없다면 새로 생성해서 필드에 저장하고 아니라면 기존의 맵에 값만 저장한다.
값을 가져올때도 마찬가지로 현재 ThreadLocal
객체의 해시코드를 활용하여 인덱싱한다.
@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()
를 통해서 꼭 제거해 주어야 한다.