ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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

    댓글

Designed by Tistory.