여러 스레드에서 데이터를 공유할 때 발생하는 문제를 해결하고자 synchronized
예약어를 사용했다. 하지만 만약 스레드별로 서로 다른 값을 처리해야한다면 어떻게 해야할까? 정답은 ThreadLocal을 이용하는 것이다. 어떻게 사용하는지 정리해보자.
의도하고자 하는 내용은 첫 번째 스레드가 0에서 10까지의 계산을 먼저하고 그에 대한 결과로 45가 출력되고, 두 번째 스레드에서 첫 번째 결과를 이어받아 90이라는 결과를 출력하는 거라고 가정해보자.
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
위의 코드에서 스레드 클래스만 수정해보자.
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
그렇다면, 첫 번재 스레드의 결과가 두 번째의 스레드 결과에 영향을 주지 않게 하려면 어떻게 해야할까? 🤔
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의 주요 코드를 뜯어보면, 각 스레드에서는 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);
}
// (...생략)
}
내가 참고한 블로그의 이미지에서는 각 스레드별로 별도의 배열이 있고, 그 배열 내부에는 ThreadLocals
과 Math::random
이라는 값이 들어가 있다.
(내가 작성한 예제에서는 ThreadLocals
과 Calculate
객체가 들어가지 않을까 싶다 🤔)
private static final
로 선언한다.remove()
메소드를 호출해주는 습관을 가져야한다.일반적으로는 스레드들이 한 번 생성되고 수행이 끝나면 사라지지만, 웹 기반의 애플리케이션에서는 스레드를 재사용하기 위해 스레드 풀을 사용하기 때문에 remove()
메소드가 필요하다. 스레드 풀을 이용하게 되면 스레드가 시작한 후에 끝나는 것이 아니기 때문에 이 메소드를 이용하여 지워줘야지만 해당 스레드를 다음에 사용할 때 쓰레기 값이 담기지 않는다.
톰캣에서는 요청 하나당 스레드 하나를 할당한다. 만약 여기서 ThreadLocal로 사용자 정보를 설정해두면, 해당 스레드를 사용할 때마다 사용자의 정보를 마음대로 꺼내 사용할 수 있어 편리함이 증가한다고 한다. 단, 잘 정리하지 않으면 다른 사람이 뭣도 모르고 사용한다면 문제가 발생할 수 있으니 주의해야한다.