ThreadLocal은 뭘까?

maketheworldwise·2022년 3월 7일
0
post-custom-banner


이 글의 목적?

여러 스레드에서 데이터를 공유할 때 발생하는 문제를 해결하고자 synchronized 예약어를 사용했다. 하지만 만약 스레드별로 서로 다른 값을 처리해야한다면 어떻게 해야할까? 정답은 ThreadLocal을 이용하는 것이다. 어떻게 사용하는지 정리해보자.

Thread - 코드로 이해해보자

의도하고자 하는 내용은 첫 번째 스레드가 0에서 10까지의 계산을 먼저하고 그에 대한 결과로 45가 출력되고, 두 번째 스레드에서 첫 번째 결과를 이어받아 90이라는 결과를 출력하는 거라고 가정해보자.

synchronized 없는 경우

public class Calculate {

    int num = 0;

    public int add() {
        for(int i = 0; i < 10; i++) {
            num += i;
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return num;
    }
}
public class ThreadSample extends Thread {

    private Calculate calculate = new Calculate();

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ": " + calculate.add());
    }
}
public class Main {

    public static void main(String[] args) {
        Runnable runnable = new ThreadSample();
        Thread sample1 = new Thread(runnable);
        Thread sample2 = new Thread(runnable);
        sample1.start();
        sample2.start();
    }
}

결과를 확인하면 다음과 같이 의도한 내용과는 다른 결과가 나오는 것을 확인할 수 있다. 그 이유는 두 개의 스레드가 동시에 접근하기 때문이다.

Thread-1: 63
Thread-2: 63

synchronized 있는 경우

위의 코드에서 스레드 클래스만 수정해보자.

public class ThreadSample extends Thread {

    private Calculate calculate = new Calculate();

    @Override
    public void run() {
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + ": " + calculate.add());
        }
    }
}

결과는 내가 의도한 대로 출력되는 것을 확인할 수 있다.

Thread-1: 45
Thread-2: 90

그렇다면, 첫 번재 스레드의 결과가 두 번째의 스레드 결과에 영향을 주지 않게 하려면 어떻게 해야할까? 🤔

ThreadLocal - 코드로 이해해보자

synchronized 예약어 없이 스레드 클래스만 수정해보자.

public class ThreadSample extends Thread {

    private static final ThreadLocal<Calculate> local = new ThreadLocal<>() {
        @Override
        protected Calculate initialValue() {
            return new Calculate();
        }
    };

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ": " + local.get().add());
        local.remove();
    }
}

결과를 확인하면, 각 스레드별로 계산한 결과가 출력되는 것을 확인할 수 있다.

Thread-1: 45
Thread-2: 45

즉, 각 스레드의 결과가 서로 영향을 끼치지 않고 각자의 스레드 안에서만 계산 결과를 출력하는 것을 알 수 있다.

ThreadLocal - 코드를 뜯어보자

ThreadLocal의 주요 코드를 뜯어보면, 각 스레드에서는 ThreadLocalMap 타입의 ThreadLocal 필드를 가지고 있는 것을 확인할 수 있다.

	// (...생략)

	// ThreadLocal.ThreadLocalMap
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
	// (...생략)
    
    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);
        }
    }

그리고 ThreadLocalMap에는 Entry 타입인 배열 table이 있다. 이 배열에는 ThreadLocals와 우리가 저장하고자 하는 값이 포함되어있다.

    static class ThreadLocalMap {
    
    	// (...생략)
        
        private Entry[] table;
        
    	// (...생략)

		private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }
        
    	// (...생략)
        
	}

내가 참고한 블로그의 이미지에서는 각 스레드별로 별도의 배열이 있고, 그 배열 내부에는 ThreadLocalsMath::random이라는 값이 들어가 있다.

(내가 작성한 예제에서는 ThreadLocalsCalculate 객체가 들어가지 않을까 싶다 🤔)

ThreadLocal 사용시 주의할 점

  • ThreadLocal에 저장된 값은 해당 스레드에서 고유하게 사용할 수 있다.
  • ThreadLocal 클래스의 변수는 private static final로 선언한다.
  • 사용이 끝나면 remove() 메소드를 호출해주는 습관을 가져야한다.

일반적으로는 스레드들이 한 번 생성되고 수행이 끝나면 사라지지만, 웹 기반의 애플리케이션에서는 스레드를 재사용하기 위해 스레드 풀을 사용하기 때문에 remove() 메소드가 필요하다. 스레드 풀을 이용하게 되면 스레드가 시작한 후에 끝나는 것이 아니기 때문에 이 메소드를 이용하여 지워줘야지만 해당 스레드를 다음에 사용할 때 쓰레기 값이 담기지 않는다.

어디서 사용할까?

톰캣에서는 요청 하나당 스레드 하나를 할당한다. 만약 여기서 ThreadLocal로 사용자 정보를 설정해두면, 해당 스레드를 사용할 때마다 사용자의 정보를 마음대로 꺼내 사용할 수 있어 편리함이 증가한다고 한다. 단, 잘 정리하지 않으면 다른 사람이 뭣도 모르고 사용한다면 문제가 발생할 수 있으니 주의해야한다.

이 글의 레퍼런스

profile
세상을 현명하게 이끌어갈 나의 성장 일기 📓
post-custom-banner

0개의 댓글