본문 바로가기
Java

[Java] ThreadLocal 은 무엇이고 왜 사용할까?

by doogfoot 2024. 10. 27.

 

정의

 

ThreadLocal 이란 멀티 스레드 환경에서 각각의 스레드별로 저장 공간을 할당하여, 독립적인 값을 저장할 수 있게 해주는 기술 및 클래스를 말합니다.

 

 

사용 이유

 

Java 나 Spring의 멀티스레드 환경에서 어떤 클래스를 싱글톤으로 인스턴스를 하나 만들었다고 가정하겠습니다. (일반적인 Bean 도 예시가 될 수 있겠네요.)

 

이때 인스턴스의 필드나 static 공용 필드를 변경할 때 동기화 문제가 발생할 수 있습니다. 

 

이는 여러 스레드에서 동시에 같은 필드(메모리 공간)를 접근하기 때문에 생기는 문제입니다.

 

이 문제를 가장 쉽게 해결하려면 각각의 공간을 만들고 각 스레드별로 자신의 저장 공간을 할당하면 됩니다

 

 

 

기본 개념

 

1. 독립성 : ThreadLocal 에 저장된 값은 해당 스레드에서만 접근이 가능하며 다른 스레드에서 참조할 수 없습니다. 따라서 멀티스레드 환경에서 동기화 문제를 해결할 수  있습니다.

 

2. 안정성 : 각 스레드는 ThreadLocal 객체에 대한 자신의 복사본을 가지고 있어서 데이터 변경 시 다른 스레드에 영향을 주거나 받지 않습니다. 

 

 

 

위의 기본 개념을 생각하면서 예시 코드를 설명드리겠습니다.

public class ThreadLocalExample {
    private static ThreadLocal<Integer> threadLocalValue = new ThreadLocal<>();

    public static void main(String[] args) {
        Runnable task = () -> {
            // 현재 스레드의 값을 설정
            threadLocalValue.set((int) (Math.random() * 100));
            System.out.println(Thread.currentThread().getName() + " - Value: " + threadLocalValue.get());
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);
        thread1.start();
        thread2.start();
    }
}

 

 

Integer 타입을 저장할 수 있는 ThreadLocal 의 인스턴스를 생성하는 방법은 아래와 같습니다

ThreadLocal<Integer> threadLocalValue = new ThreadLocal<>();

 

 

또한 ThreadLocal 에서 제공하는 get, set 메서드를 통해 데이터를 저장하고 가져올 수 있습니다.

예시로 ThreadLocal 에서 정수 30을 저장하고 가져오는 코드는 아래와 같습니다

threadLocalValue.set(30);
Integer value = threadLocalValue.get(); // 30

 

 

ThreadLocal에 저장된 데이터를 제거하는 방법은 아래와 같습니다

threadLocalValue.remove();

 

 

 

ThreadLocal 클래스의 내부 구현을 보면 조금 더 쉽게 원리에 대해 이해할 수 있습니다. 

public class Thread implements Runnable {
	ThreadLocal.ThreadLocalMap threadLocals = null;
}

 

Java의 Thread 객체는 ThreadLocal의 ThreadLocalMap 을 필드로 가지고 있습니다.

즉 아래와 같이 ThreadLocal 을 Key로 하는 Map을 통해 데이터를 저장하고 있습니다. 

public class ThreadLocal<T> {
	ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) { 
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

    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();
    }

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

	static class ThreadLocalMap {
		static class Entry extends WeakReference<ThreadLocal<?>> {            
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
	}
}

 

 

 

 

주의 사항

 

1. 사용 후 명시적 삭제 : ThreadLocal 은 스레드가 종료되더라도 저장된 값이 남아 있을 수도 있습니다. 따라서 스레드 풀을 사용하는 경우 명시적으로 데이터를 제거(remove)하는 것이 좋습니다. 

2. 적절한 사용 : ThreadLoca 은 필요한 경우에만 사용하는 것이 좋습니다. 많은 스레드가 동일안 메모리 공간을 공유해야 되는 경우 다른 동기화 방법을 생각해야 됩니다

3. 성능 저하 : ThreadLocal 은 메모리 사용을 증가시킵니다. 사용 빈도와 데이터 양에 따라 성능에 영향을 줄 수 있습니다.

 

주의사항 1번에 대해서 풀어서 설명드리겠습니다.

스레드 풀이 아닌 일반적인 경우 매 요청마다 스레드가 생성되고 응답이 끝나면 스레드가 소멸됩니다.

이 경우는 ThreadLocal에 저장된 데이터를 접근할 수 없기 때문에 문제가 되지 않습니다.

 

반면 스레드풀을 사용하는 경우 응답을 마친 스레드는 다시 풀에 들어가 재사용됩니다.

스레드가 소멸되지 않기 때문에 스레드가 참조하고 있는 ThreadLocal 값 또한 GC에 의해 자동으로 제거되지 않습니다.

그래서 ThreadLocal을 다 사용하고 나면 remove() 메서드로 다른 스레드가 사용할 수 없도록 명시적으로 제거해야 합니다.

 

 

마무리

 

ThreadLocal 이란 멀티 스레드 환경에서 동시성 문제를 해결할 수 있는 기술 중 하나입니다.

각각의 스레드별로 저장 공간을 할당하여, 독립적인 값을 저장할 수 있어서 상황에 맞게 사용한다면 편하고 안전한 시스템을 개발할 수 있습니다.

 

댓글