여러 쓰레드가 동시에 같은 인스턴스의 필드 값을 변경하면서 발생하는 문제를 동시성 문제라고 한다. 이러한 동시성 문제는 여러 쓰레드가 같은 인스턴스의 필드에 접근해야 하기 때문에 트래픽이 점점 많아질수록 자주 발생한다.
현업에서 종종 발생하는 문제로, 특히 스프링 빈처럼 싱글톤 객체의 필드를 변경하며 사용할때 이러한 동시성 문제를 조심해야 한다.
이런 동시성 문제는 지역 변수에서는 발생하지 않는다. 지역 변수는 쓰레드마다 각각 다른 메모리 영역이 할당된다
동시성 문제가 발생하는 곳은 같은 인스턴스의 필드(주로 싱글톤), 또는 static같은 공용 필드에 접근할 때 발생한다.
동시성 문제는 값을 읽기만 하면 발생하지 않는다. 어디선가 값을 변경하기 때문에 발생한다.
지금의 문제를 해결하기 위해서, 각 쓰레드마다 고유의 저장소를 만들 수 있다. 이것이 ThreadLocal이다.
데이터 저장.
위의 사진과 같이 thread-A가 userA라는 값을 저장하면 쓰레드 로컬은 thread-A 전용 보관소에 데이터를 안전하게 보관한다.
thread-B가 userB라는 값을 저장하면 쓰레드 로컬은 thread-B 전용보관소에 데이터를 안전하게 보관한다.
데이터 조회
쓰레드 로컬을 통해서 데이터를 조회할 때도 thread-A가 조회하면 쓰레드 로컬은 thread-A 보관소에서 userA 데이터를 반환해준다. 물론 thread-B가 조회하면 thread-B 전용보관소에서 userB 데이터를 반환해준다.
@Sl4fj
public class ThreadLocalService{
private ThreadLocal<String> nameStore = new ThreadLocal<>();
public String logic(String name){
log.info("저장 name={} -> nameStore={}",name,nameStore.get());
nameStore.set(name);
sleep(1000);//1초동안 sleep
log.info("조회 nameStore={}",nameStore.get());
return nameStore.get();
}
}
ThreadLocalService를 살펴 보도록 하자.
테스트 코드를 통해 확인해 보도록 하자.
@Sl4fj
public class ThreadLocalTest{
private ThreadLocalService service = new ThreadLocalService();
@Test
void test(){
log.info("main start");
Runnable userA = () -> {
service.logic("userA");
};
Runnable userB = () -> {
service.logic("userB");
};
Thread threadA = new Thread(userA);
threadA.setName("thread-A");
Thread threadB = new Thread(userB);
threadB.setName("thread-B");
threadA.start();
sleep(100);
threadB.start();
sleep(3000);
log.info("main exit");
}
코드 설명
1. Runnable은 람다식을 통해 쓰레드가 어떤 동작을 할지를 정의하였다.
2. thread.setName: 로그에서 확인할 수 있도록 thread 이름을 설정하였다.
3. Thread thread = new Thread(): 쓰레드는 생성하기만 하면 시작하는 것이 아니라, thread.start();를 통해 쓰레드를 직접 시작한다.
4. sleep(3000): 테스트를 실행하면 main 쓰레드가 threadA와 threadB를 실행하는 구조이다. 마지막에 sleep 3초를 넣은 이유는 그렇지 않으면, main 쓰레드가 먼저 종료되면 JVM이 종료되어서 실행중이던 threadA, threadB가 실행중에 종료되어 조회를 하지 못하고 로그가 아래와 같이 생긴다.
반면 sleep을 통해 메인 쓰레드가 threadA, threadB가 종료되기를 기다리게 하면 정상적인 로그를 확인할 수 있다.
해당 쓰레드가 쓰레드 로컬을 모두 사용하고 나면 Thread.remove()를 통해 쓰레드 로컬에 저장된 값을 제거해주어야 한다.
이유: WAS는 사용이 끝난 쓰레드를 쓰레드 풀에 반환한다. 쓰레드를 생성하는 비용이 비싸기 때문에 보통 쓰레드 풀을 통해서 쓰레드를 재사용한다.
만약 전에 사용한 쓰레드에서 threadlocal.remove()를 통해 쓰레드 로컬의 값을 제거하지 않으면, 쓰레드 풀에 반환된 쓰레드에는 전에 사용된 데이터들이 남아있게 되어 문제가 발생한다.
nameStore.remove();
를 통해 다 사용한 ThreadLocal 데이터를 삭제하고 쓰레드 풀에 반환해야 한다.