본문 바로가기

JAVA

쓰레드의 지역변수 ThreadLocal

우리는 앞선 포스팅에서 멀티 쓰레드 환경에서 데이터를 공유할때 발생하는 문제를 synchronized 라는 구문을 사용하여 해결하였다.

쓰레드(3) - Synchronized

 

쓰레드(3) - Synchronized

1. 쓰레드와 밀접한 연관이 있는 Synchronized synchronized는 자바의 예약어 중 하나이다. 즉, 변수명, 클래스명으로 사용이 불가능하다. StringBuffer와 StringBuilder의 차이점으로 StringBuffer는 "쓰레드에..

dev-cool.tistory.com

 

쓰레드 별로 서로 다른값을 처리해야 하는 상황에는 어떻게 처리할 것인가?

 

제목을 보고 눈치챈 사람도 있겠지만 위와같은 의문점을 해결하기 위해서 우리는 ThreadLocal 을 사용한다.

 

1. ThreadLocal


synchronized 를 사용한 동기화 방법은 한 스레드의 작업이 끝나기 전까지 다른 스레드의 접근을 막아준다.

하지만, 각각의 스레드 전역에서 공유되어야 하는 데이터가 필요한 상황에서 어떻게 해야할까?

heap 영역에 보관해서 공유하면 thread-safe 가 무너지게 될것이고,
그렇다고 매번 stack 영역에 선언하고 사용한 후 버리면 안되는 경우도 있기 마련이다.

이러한 상황에서 사용하는 것이 ThreadLocal 이다.

구체적인 코드를 보면서 사용법을 알아보자.

public class UserInfo {
    private String id;

    //기본 생성자로 id 에 random 값 삽입
    public UserInfo() {
        Random random = new Random();
        this.id = String.valueOf(random.nextInt(30));
    }

    public String getId() {
        return id;
    }
}

public class UserInfoContext {
	// UserInfo 타입 ThreadLoacl 변수를 생성
    public static ThreadLocal<UserInfo> userInfoThreadLocal = new ThreadLocal<>();
}

 

데이터를 담는 객체를 생성하기 위해서 UserInfo 클래스를 생성하였고,
UserInfoContext 클래스에 static 변수로 ThreadLocal 변수를 선언하였다.

 

public class UserServiceA {
    private UserServiceB userServiceB;
	
    //logicA() 메소드가 수행되면서 동일한 쓰레드에서 logicA(), logicB() 메소드 실행
    public void logicA(){
        UserInfoContext.userInfoThreadLocal.set(new UserInfo());
        userServiceB = new UserServiceB();
        userServiceB.logicB();

        System.out.println("logicA 의 user id : "+UserInfoContext.userInfoThreadLocal.get().getId());

        UserInfoContext.userInfoThreadLocal.remove();
    }
}

public class UserServiceB {
    public void logicB() {
        UserInfo userInfo = UserInfoContext.userInfoThreadLocal.get();

        System.out.println("logicB 의 user id : " + userInfo.getId());
    }
}

public class UserServiceC {
	
    //다른 쓰레드에서 logicC() 메소드 실행
    public void logicC(){
        UserInfoContext.userInfoThreadLocal.set(new UserInfo());

        System.out.println("logicC 의 user id : "+UserInfoContext.userInfoThreadLocal.get().getId());

        UserInfoContext.userInfoThreadLocal.remove();
    }
}

 

logicA(), logicC() 메소드 실행결과

 

3개의 클래스를 작성하였는데 logicA() 메소드에서 logicB() 메소드로 파라미터로 값을 넘긴것이 아닌,
ThreadLoacl 변수에서 동일 스레드는 같은 자원을 공유하여 값을 가져온것을 확인할 수 있다.

하지만 logicC() 의 경우 다른 쓰레드에서 동작을 수행하기 때문에 다른 user id를 받아오는것을 확인할 수 있다.

 

2. ThreadLocal 의 동작원리


그렇다면 ThreadLocal 클래스는 synchronized 없이 어떻게 구현했을지 궁금해서 한번 내부 코드를 살펴보았다.

 

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

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

 

ThreadLocal 클래스는 set() 메소드를 수행하게 되면 실제 getMap() 메소드로 실제 Thread 클래스에 있는 변수에
ThreadLocalMap 타입의 값을 할당한다.

ThreadLocalMap 클래스는 ThreadLocal 의 내부 클래스로 초기에는 16개의 크기를 가지는 Entry 배열로 Thread에 저장할 값을 담는 공간이다.

ThreadLocal 변수랑 동기화 작업 없이 Thread 자체에 값을 할당하여 원천적으로 동기화 작업이 필요없게 작성된 것이다.

 

3. 주의사항


일반적으로 Thread Pool 환경에서 ThreadLocal 을 사용한다면, Thread Pool 를 통해 Thread가 재사용 되지 않도록 remove() 메소드를 사용해서 반드시 ThreadLocal 변수를 초기화해서 다음 요청에 영향이 가지 않게 해야한다.