-
ThreadLocal🌱 spring 2023. 1. 18. 19:08
📌 ThreadLocal
💡 한 스레드에 의해서만 일고 쓰일 수 있는 변수
서로 다른 두 스레드가 하나의 변수에 접근했을 때, 해당 변수는 공유자원으로 동시성 문제가 발생할 가능성이 있다.
ThreadLocal은 오직 한 스레드에 의해 읽고/쓰일 수 있는 변수를 생성한다.
- 두 스레드가 같은 코드를 실행하고 이 코드가 하나의 threadLocal 변수를 참조하더라도 서로의 threadLocal 변수를 볼 수 없다.
예시
ThreadA가 ThreadLocal 변수에 접근하여 1이라는 값을 저장.
동시에 ThreadB가 ThreadLocal 변수에 접근해 2라는 값을 저장1이라는 값을 2가 덮는 것이 아니다 ❌
각 스레드별 전용 저장소에 각각의 값이 저장된다.
⇒ 멀티 스레드 환경에서 공유 자원의 동시성 문제를 해결할 수 있다.
📌 스레드 로컬 라이프 사이클
ThreadLocal
public class ThreadLocal<T> { 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(); } public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { map.set(this, value); } else { createMap(t, value); } } public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) { m.remove(this); } } ... }
생성
ThreadLocal<String> threadLocal = new ThreadLocal<>();
제네릭을 사용해 타입을 지정할 수 있다.
쓰기 / 읽기
threadLocal.set("A"); // 값 쓰기 threadLocal.get(); // 값 읽기
threadLocal 변수의 set(), get() 메서드를 통해 값을 쓰고 읽을 수 있다.
제거
threadLocal 을 모두 사용하고 나면 반드시 저장된 값을 제거해주어야 한다.
- ThreadLocal.remove()
사용
@Slf4j public class ThreadLocalTest { @Test void test() { for(int threadNum = 1; threadNum <= 5; threadNum++) { TestThread testThread = new TestThread("thread-" + threadNum); testThread.start(); } } private static class TestThread extends Thread { private static final ThreadLocal<String> testThreadLocal = ThreadLocal.withInitial(() -> "init"); private final String name; public TestThread(String name) { this.name = name; } @Override public void run() { log.info("== {} start, ThreadLocal: {} ", name, testThreadLocal.get()); testThreadLocal.set(name); log.info("== {} finish, ThreadLocal: {} ", name, testThreadLocal.get()); } } }
결과
- 스레드간 간섭 없이 값이 제대로 저장되었다.
⚠️ 스레드 풀 사용시 주의
스레드 로컬은 스레드 풀을 사용하는 환경에서 주의해야 한다.
스레드가 종료된 후 스레드 풀에 반납되어 재사용될 수 있기 때문에 사용이 끝나고 나면 스레드 로컬을 비워주는 과정이 있어야 한다.
thread 사용을 끝낸 후 thread는 종료되지 않고 thread pool에 반납이 된다.
따라서, 해당 스레드는 재사용되기 때문에 저장된 값을 제거해주어야 한다.
- 그렇지 않으면 재사용되는 스레드가 올바르지 않은 데이터를 참조할 수 있다.
스레드 풀을 사용해 스레드를 실행시키는 예시
@Test void threadPoolTest() { final ExecutorService executorService = Executors.newFixedThreadPool(3); for(int threadNum = 1; threadNum <= 5; threadNum++) { String name = "thread-" + threadNum; TestThread thread = new TestThread(name); executorService.execute(thread); } // 스레드 풀 종료 executorService.shutdown(); // 스레드 풀 종료 대기 while (true) { try { if (executorService.awaitTermination(10, TimeUnit.SECONDS)) { break; } } catch (InterruptedException e) { log.error("==== Error : {} ====", e); executorService.shutdownNow(); } } log.info("All threads are finish"); }
결과
- thread4 가 시작했을 때 ThreadLocal의 값이 ‘init’이 아닌 thread-1 으로 되어있다.
- 이미 스레드 로컬에 값이 들어가 있다.
이런 결과가 나온 이유는 스레드 풀을 통해서 스레드가 재사용 되기 때문이다.
📌 스레드 로컬 사용 예시
Spring Security
SecurityContextHolder
- ThreadLocal을 이용해 사용자 인증 정보를 전파
public class SecurityContextHolder { ... private static void initializeStrategy() { if (MODE_PRE_INITIALIZED.equals(strategyName)) { Assert.state(strategy != null, "When using " + MODE_PRE_INITIALIZED + ", setContextHolderStrategy must be called with the fully constructed strategy"); return; } if (!StringUtils.hasText(strategyName)) { // Set default strategyName = MODE_THREADLOCAL; } if (strategyName.equals(MODE_THREADLOCAL)) { strategy = new ThreadLocalSecurityContextHolderStrategy(); return; } if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) { strategy = new InheritableThreadLocalSecurityContextHolderStrategy(); return; } if (strategyName.equals(MODE_GLOBAL)) { strategy = new GlobalSecurityContextHolderStrategy(); return; } ... } ... }
Transaction Manager
- 트랜잭션 컨텍스트를 전파하는데 threadLocal을 사용
RequestContextHolder 클래스
- 어느 layer에서든 HttpServletRequest를 조회할 수 있다.
public abstract class RequestContextHolder { private static final boolean jsfPresent = ClassUtils.isPresent("javax.faces.context.FacesContext", RequestContextHolder.class.getClassLoader()); private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal<>("Request attributes"); private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal<>("Request context"); ... }
참고 자료
https://junior-datalist.tistory.com/275
https://coding-start.tistory.com/117
https://javacan.tistory.com/entry/ThreadLocalUsage
https://madplay.github.io/post/java-threadlocal
'🌱 spring' 카테고리의 다른 글
🍀 MongoDB - QueryDsl with Spring (0) 2023.05.30 🐱 Tomcat (0) 2023.03.12 🌊 Connection Pool (0) 2023.01.13 JDBC ❓ (0) 2023.01.13 Spring Interceptor에서 Request 데이터 처리 (1) 2022.12.06