[JAVA] ThreadLocal

반응형
728x90
반응형

ThreadLocal

ThreadLocal 클래스에는 get, set 메서드가 있는데 이를 호출하는 스레드마다 다른 값을 사용할 수 있도록 관리해준다. ThreadLocal 클래스의 get 메서드를 호출하면 현재 실행중인 스레드에서 최근에 set 메서드를 호출해 저장했던 값을 가져올 수 있다.

스레드 로컬 변수는 변경 가능한 싱글턴이나 전역 변수 등을 기반으로 설계되어있는 구조에서 변수가 임의로 공유되는 상황을 막기위해 사용하는 경우가 많다. 쉽게말해서, ThreadLocal이란 스레드 단위로 로컬 변수를 제공하는 클래스다.

 

 

ThreadLocal 동작방식

https://i.stack.imgur.com/Dhws6.jpg

 

ThreadLocalMap

스레드가 코드를 실행하면서 만나게되는 모든 ThreadLocal 변수를 threadLocals 객체에 저장한다.

ThreadLocalMap은 Thread가 가지고 있다.

public class Thread implements Runnable {
    ...

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    ...
}

threadLocals의 모습은 아래와 같다.

https://codingdog.tistory.com/entry/java-threadlocal-%ED%81%B4%EB%9E%98%EC%8A%A4%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C-%EA%B0%84%EB%8B%A8%ED%95%98%EA%B2%8C-%EC%95%8C%EC%95%84%EB%B4%85%EC%8B%9C%EB%8B%A4

thread 안에서 별도로 관리되고있는 threadLocals를 얻어온다.

 

https://junghyungil.tistory.com/100

 

 

ThreadLocal.get()

▶ 특정 스레드가 ThreadLocal.get() 메소드를 처음 호출한다면 initiaValue 메서드에서 값을 만들어, 해당 스레드에게 초기 값을 넘겨준다

 

1) get 메서드

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();
}
  • currentThread() : 현재의 쓰레드를 얻어온다.
  • getMap() : ThreadLocalMap 타입인 현재 스레드의 threadLocals 객체를 가져온다.

 

getMap()
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
  • 얻어온 ThreadLocalMap 객체가 비어져있으면 setInitialValue() 메서드로 초기화한다.
  • 얻어옫 ThreadLocalMap 객체가 존재한다면 ThreadLocalMap.Entry 객체를 얻어와서 value를 리턴한다.

 

getEntry()

hash 값으로 table 배열의 인덱스를 찾아서 Entry를 얻어온다. 

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

 

2) 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);
    }
    if (this instanceof TerminatingThreadLocal) {
        TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
    }
    return value;
}

 

3) getMap()

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

 

4) createMap()

최초 호출일 경우 수행된다.

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

 

ThreadLocal.set()

1) ThreadLocal.set()

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}
  • currentThread() : 현재의 쓰레드를 얻어온다.
  • getMap() : ThreadLocalMap 타입인 현재 스레드의 threadLocals 객체를 가져온다.

 

getMap()
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
  • 얻어온 ThreadLocalMap 객체가 비어져있으면 createMap() 메서드를 호출한다.
  • 얻어온 ThreadLocalMap 객체가 존재한다면 set 메서드로 key, value 를 세팅한다.

 

2) ThreadLocalMap.set()

현재 Thread 내부에 있는 Map 자료구조에 key : ThreadLocal 로 value를 저장한다.

private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

 

 

정리

ThreadLocal 동작원리 때문에, ThreadLocal 변수들은 많은 Thread가 동시에 사용한다 해도, 각 Thread가 가지고 있는 Map에 key, value로 사용하고 value는 Thread가 각각의 ThreadLocalMap을 가지고있기 때문에 저장되는 값이 다르다.

 

개념적으로 본다면 ThreadLocal<T> 클래스는 Map<Thread, T>라는 자료구조로 구성되어있고, Map<Thread, T>를 사용하여 스레드별 값을 보관한다.
Map<Thread, T> 를 사용해 구현되어있다는 말은 아니다. 스레드별 값은 실제로 Thread 객체 자체에 저장되어있다.
스레드가 종료되면 스레드별 값으로 할당되어 있던 부분도 가비지 컬렉터가 처리한다.

단일 스레드에서 동작하던 기능을 멀티스레드 환경으로 구성해야할때, 그 의미에 따라 다르지만 공유된 전역 변수를 ThreadLocal을 활용하도록 변경하면 스레드 안정성을 보장할 수 있다.

 

예시1

JDBC 연결시, 멀티스레드 어플리케이션에서 적절한 동기화 없이 연결 객체를 전역변수로 만들어 사용하면 스레드 안전하지 않다.
이때 ConnectionHolder와 같이 JDBC 연결을 보관할때 ThreadLocal을 사용하면 스레드는 저마다 각자의 연결 객체를 갖게된다.

 

예시2

현재 진행중인 트랜잭션이 어느것인지 확인하고 싶을때 트랜잭션이 보관되어있는 ThreadLocal 클래스에서 쉽게 찾아낼 수 있다. 이런식으로 전역변수가 아니면서도 전역변수로 동작하기 때문에 조심해야할 필요는 있다.


ThreadLocal를 사용할때 재사용성을 크게 떨어뜨릴 수 있고, 객체간에 눈에 보이지 않는 연결 관계를 만들어내기 쉽기 때문에 정확하게 영향도를 파악하여 사용해야한다.

 

 

 

반응형

Designed by JB FACTORY