Java ThreadLocal

허진혁·2023년 7월 23일
0

기본기를 다지자

목록 보기
6/10
post-thumbnail

🤔 궁금 사항

자바에서 스레드(thread)마다 독립적인 변수를 가질 수 있게 해주는 스레드 로컬(thread local)은 무엇일까?

사용할 때 무엇을 고려해야할까?

Thread

Thread 클래스 내부에 ThreadLocalMap을 맴버 변수로 갖고 있어요.ThreadLocal 객체와 스레드 간의 연결을 관리해주는 map 이에요.

// Thread 클래스
public class Thread implements Runnable {
	// ...
	/* ThreadLocal values pertaining to this thread. This map is maintained
	 * by the ThreadLocal class. */
	ThreadLocal.ThreadLocalMap threadLocals = null;
}

ThreadLocal

ThreadLocal은 각각의 스레드에게 독립적인 값을 제공하기 위해 사용되는 클래스에요. 각 스레드는 자신만의 데이터를 가지고 있으며, 다른 스레드의 데이터에 접근하지 않아요. ThreadLocal을 사용하면 스레드 간의 동기화 문제를 해결할 수 있고, 동시성을 높일 수 있어요.

withInitial() 메서드

Java 8에서 도입된 메서드로 ThreadLocal 객체에 접근할 때 호출되며, 해당 스레드의 초기 값으로 사용되요. initialValue() 메서드도 같은 목적을 갖고 있지만, withInitial()메서드를 사용하면 람다식을 사용하여 더욱 간결하고 가독성 좋은 코드가 돼요.

public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}

set(), get() 메서드

get() 메서드는 값을 가져오고, set()메서드는 ThreadLocal을 key에 value 값을 저장해요.

public class ThreadLocal<T> {
	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 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();
	}
	
	ThreadLocalMap getMap(Thread t) {
	    return t.threadLocals;
	}
	
	void createMap(Thread t, T firstValue) {
	    t.threadLocals = new ThreadLocalMap(this, firstValue);
	}
}

remove() 메서드

스레드 로컬 변수 값을 삭제하는 메서드에요. 스레드 풀(thread pool)을 사용하는 환경에서는 스레드 로컬 변수 사용이 끝났다면 remove() 메서드를 명시적으로 호출해야 해요. 스레드가 재활용되면서 이전에 설정했던 스레드 로컬 정보가 남아있을 수 있기 때문이에요.

public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         m.remove(this);
 }

ThreadLocalMap

ThreadLocalMap은 ThreadLocal클래스의 정적 내부 클래스에요. 내부적으로 해시 테이블 정보를 갖고 있으며, 요소는 WeakReference를 확장하고 ThreadLocal 객체를 키로 사용하는 Entry 클래스에요.

ThreadLocal.ThreadLocalMap은 각 스레드 객체에 하나씩 생성되며, 해당 스레드의 모든 ThreadLocal 객체의 값을 저장하는 역할이에요.

public class ThreadLocal<T> {
    // ...생략
	static class ThreadLocalMap {
		// ...생략
		static class Entry extends WeakReference<ThreadLocal<?>> {
    	/** The value associated with this ThreadLocal. */
      Object value;

      Entry(ThreadLocal<?> k, Object v) {
          super(k);
          value = v;
      }
		}
	}
}

ThreadLocalMap의 기능과 동작은 다음과 같아요.

  • 데이터 저장 및 접근

각 스레드 내에서 각 ThreadLocal 객체에 대한 값을 저장하고 접근할 수 있어요.

  • 스레드 간 데이터 격리

ThreadLocal 객체를 사용하여 스레드 간에 객체를 공유하지 않고 독립적으로 유지 되요. 그래서 스레드 사이의 데이터 격리를 제공하여 스레드 안정성을 강화해요.

  • 스레드가 종료되면 자동 제거

ThreadLocal 객체에 저장된 값은 해당 스레드가 종료되면 자동으로 정리되므로 메모리 누수를 방지하고 자원 관리를 편하게 해줘요.
❗️ThreadLocal이 비워지지 않으면 생기는 문제점은 꼭 알아야 하기에 하위 부분에 다시 정리할게요.

ThreadLocal 사용해보기

public class ThreadLocalTest {
    static class TestThread extends Thread{
        public static final ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "testName");
        private final String name;

        public TestThread(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            System.out.printf("%s start. ThreadLocal: %s%n", name, threadLocal.get());
            threadLocal.set(name);
            System.out.printf("%s finish. ThreadLocal: %s%n", name, threadLocal.get());
        }
    }

    public void printRun() {
        for (int i = 1; i <= 5; i++) {
            TestThread thread = new TestThread("thread-" + i);
            thread.start();
        }
    }

    public static void main(String[] args) {
        new ThreadLocalTest().printRun();
    }
}

실행시킬때 마다 다른 순서가 나오지만 스레드 간에 간섭없이 이름은 잘 저장돼요.

thread-2 start. ThreadLocal: testName
thread-2 finish. ThreadLocal: thread-2
thread-5 start. ThreadLocal: testName
thread-5 finish. ThreadLocal: thread-5
thread-4 start. ThreadLocal: testName
thread-4 finish. ThreadLocal: thread-4
thread-3 start. ThreadLocal: testName
thread-3 finish. ThreadLocal: thread-3
thread-1 start. ThreadLocal: testName
thread-1 finish. ThreadLocal: thread-1

❗️ 스레드 풀(Thread Pool)을 사용할 때의 주의사항

ThreadLocal은 스레드 풀(thread pool)을 사용하는 환경에서는 주의해야 해요. 스레드가 재활용될 수 있기 때문에 사용이 끝났다면 스레드 로컬을 비워주는 과정을 명시적인 코드로 표현하는 것이 필수에요.

어떤 상황이 발생할 수 있는지 다음 예제로 알아보아요.

public class ThreadLocalTest {
    // 스레드 생성 및 관리를 ExecutorService에게 위임
    private final ExecutorService executorService = Executors.newFixedThreadPool(3);
    private final int MAX_CHECK = 3;
    private int checkIdx = 0;
    static class TestThread extends Thread{
        public static final ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "testName");
        private final String name;

        public TestThread(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            System.out.printf("%s start. ThreadLocal: %s%n", name, threadLocal.get());
            threadLocal.set(name);
            System.out.printf("%s finish. ThreadLocal: %s%n", name, threadLocal.get());
        }
    }

    public void printRun() {
        for (int i = 1; i <= 5; i++) {
            TestThread thread = new TestThread("thread-" + i);
            executorService.execute(thread);
        }

        // 스레드 풀 종료
        executorService.shutdown();

        // 스레드 풀 종료 대기
        try {
            if (executorService.awaitTermination(10, TimeUnit.SECONDS)) {
                break;
            }
        } catch (InterruptedException e) {
            // 로깅을 활용해야 하지만 print문으로 대체
            System.err.println("Error: " + e);
            // 활동중인 스레드 모두 종료 -> 비정상적인 결과 초래할 가능성 있음
            executorService.shutdownNow();
        }
        System.out.println("All thread are finished");
    }

    public static void main(String[] args) {
        new ThreadLocalTest().printRun();
    }
}

결과

thread-1 start. ThreadLocal: testName
thread-1 finish. ThreadLocal: thread-1
thread-3 start. ThreadLocal: testName
thread-3 finish. ThreadLocal: thread-3
thread-5 start. ThreadLocal: thread-3 // here
thread-5 finish. ThreadLocal: thread-5
thread-2 start. ThreadLocal: testName
thread-2 finish. ThreadLocal: thread-2
thread-4 start. ThreadLocal: thread-1 // here
thread-4 finish. ThreadLocal: thread-4
All thread are finished

이것도 역시 실행시킬때 마다 다른 순서가 나와요. 하지만, 위 결과를 보면 스레드 5번과 4번이 시작되는 부분을 보면 ThreadLocal 객체의 값이 이미 들어와 있어요.

❗️ 이 부분이 스레드 풀을 사용할 때의 주의사항이에요. 다음 시나리오를 통해 알아보아요.

  1. 응용 프로그램이 스레드 풀에서 스레드를 빌립니다.
  2. 그런 다음 일부 스레드 제한 값을 현재 스레드의 ThreadLocal에 저장합니다.
  3. 현재 실행이 완료되면 응용 프로그램은 빌린 스레드를 풀에 반환합니다.
  4. 잠시 후 애플리케이션은 다른 요청을 처리하기 위해 동일한 스레드를 빌립니다.
  5. 응용 프로그램이 마지막으로 필요한 정리를 수행하지 않았기 때문에 새 요청에 대해 동일한 ThreadLocal 데이터 를 다시 사용할 수 있습니다.

즉, 스레드 풀을 통해 스레드가 재사용되기 때문이에요. 이러한 문제 때문에 remove() 메서드를 명시적으로 호출해야 해요.

@Override
	public void run() {
		System.out.printf("%s start. ThreadLocal: %s%n", name, threadLocal.get());
		threadLocal.set(name);
		System.out.printf("%s finish. ThreadLocal: %s%n", name, threadLocal.get());
		threadLocal.remove();
  }

결과

thread-1 start. ThreadLocal: testName
thread-1 finish. ThreadLocal: thread-1
thread-3 start. ThreadLocal: testName
thread-3 finish. ThreadLocal: thread-3
thread-2 start. ThreadLocal: testName
thread-2 finish. ThreadLocal: thread-2
thread-5 start. ThreadLocal: testName
thread-5 finish. ThreadLocal: thread-5
thread-4 start. ThreadLocal: testName
thread-4 finish. ThreadLocal: thread-4
All thread are finished

remove() 메서드를 통해 사용이 끝난 후 threadLocal 값을 제거함으로써 예측 가능한 결과를 만들 수 있어요.

참고자료

자바의 신

ThreadLocal in Java

김영한님의 ‘스프링 핵심 원리 - 고급편’

자바 공식문서

profile
Don't ever say it's over if I'm breathing

2개의 댓글

comment-user-thumbnail
2023년 7월 23일

잘 봤습니다. 좋은 글 감사합니다.

답글 달기
comment-user-thumbnail
2023년 7월 26일

감사합니다 :)

답글 달기