자바를 이용해 멀티 스레드 프로그래밍을 하면 성능향상을 얻을 수 있다. 하나의 스레드에서 해야할 작업들을 여러스레드로 나누어 동시에 처리하면 훨씬 빠른 시간안에 작업을 마무리할 수 있다. 하지만 멀티 스레드 프로그래밍에서 가장 고민해야할 부분은 공유 리소스에 대한 동시성 제어다. 여러 스레드가 공통으로 접근하는 변수 값을 저장하고 읽는 과정을 동기화해야 문제가 생기지 않는다.
자바는 synchronized 키워드를 이용해서 특정 메소드 혹은 코드 블럭을 동기화 시키는 방법을 제공한다. synchronized 키워드와 더불어 자바에서 제공하는 도구로 ThreadLocal이 있다. ThreadLocal은 각 스레드가 자신만 접근할 수 있는 변수로 여러 스레드가 동시에 접근할 수 있지만 각자 자기만의 공간을 사용하도록하는 장치다.
다음 코드를 살펴보자.
public class ThreadLocalTest implements Runnable {
static class MyInfo {
private String value;
public MyInfo(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
private static final ThreadLocal<MyInfo> myInfo = new ThreadLocal<MyInfo>() {
@Override
protected MyInfo initialValue() {
return new MyInfo("defaultName");
}
};
public void run() {
System.out.println("Start thread name=" + Thread.currentThread().getName() + ", myInfo=" + myInfo.get().getValue());
myInfo.set(new MyInfo("newValue From " + Thread.currentThread().getName()));
System.out.println("End thread name=" + Thread.currentThread().getName() + ", myInfo=" + myInfo.get().getValue());
}
public static void main(String[] args) {
ThreadLocalTest runnable = new ThreadLocalTest();
for (int i = 0; i < 10; i++) {
Thread t = new Thread(runnable, "" + i);
t.start();
}
}
}
이 코드를 실행하면 다음 결과를 얻게 된다.
Start thread name=0, myInfo=defaultName
Start thread name=2, myInfo=defaultName
End thread name=2, myInfo=newValue From 2
Start thread name=3, myInfo=defaultName
End thread name=3, myInfo=newValue From 3
Start thread name=7, myInfo=defaultName
Start thread name=5, myInfo=defaultName
End thread name=5, myInfo=newValue From 5
Start thread name=6, myInfo=defaultName
End thread name=6, myInfo=newValue From 6
Start thread name=4, myInfo=defaultName
Start thread name=9, myInfo=defaultName
Start thread name=8, myInfo=defaultName
End thread name=7, myInfo=newValue From 7
Start thread name=1, myInfo=defaultName
End thread name=0, myInfo=newValue From 0
End thread name=1, myInfo=newValue From 1
End thread name=8, myInfo=newValue From 8
End thread name=9, myInfo=newValue From 9
End thread name=4, myInfo=newValue From 4
ThreadLocalTest 클래스의 private static으로 선언하여 여러 스레드에서 접근할 수 있도록 만들었다. 총 10개의 스레드가 생성되어 run() 메소드를 실행하는데, 이 메소드에서 myInfo 멤버에 접근한다. 일반적이라면 동시에 여러 스레드가 하나의 멤버 변수에 접근하는 형태로 동시성 문제가 발생할 수 있다.
하지만 접근하는 변수가 ThreadLocal 객체를 사용하는 변수인 것을 주목해야한다. run() 메소드 안에서 myInfo.get() 메소드를 호출해서 실행하는 스레드가 자기자신에게 할당된 myInfo 객체를 얻어오고, myInfo.set() 메소드를 이용해서 자신의 myInfo 값을 새로 세팅한 다음 다시 myInfo.get()으로 얻어와서 값을 확인한다. 만약 스레드의 실행이 섞이면 설정한 값 대신 다른 스레드가 설정한 값이 보이는 현상이 나올 것이다.
코드에서 synchronized 키워드를 사용하지 않았기 때문에 사용자 레벨에서 추가한 동기화 메커니즘은 따로 없다. 하지만 실행결과를 해석해보면 서로 다른 스레드에게 간섭을 일으키지 않았다는 것을 알 수 있다.
생성된 스레드는 모두 공통적으로 private static ThreadLocal myInfo 변수에 접근하여 get(), set() 메소드를 사용한다. 각 스레드들은 자신에게 할당된 myInfo 정보에 접근하게 되므로 다른 스레드의 myInfo 정보에 간섭을 할 수 없다. 따라서 별도의 동기화 없이도 서로 다른 스레드의 간섭없이 myInfo를 사용할 수 있다.
이 말은 반대로 다른 스레드가 사용하는 myInfo 값에 바로 접근할 수 없다는 얘기기도 하다. 이런 기능이 필요하다면 별도로 ThreadLocal이 아닌 변수를 이용해야한다.
ThreadLocal의 동작 방식
synchronized 키워드 없이 어떻게 동기화를 구현했는지 궁금해서 코드를 열어봤다. (IntelliJ, Eclipse 같은 IDE를 이용하면 내부 코드를 쉽게 볼 수 있다. )
ThreadLocal 변수는 Thread 클래스와 밀접하게 관련있다. Thread.currentThread() 메소드를 호출하면 현재 실행중인 내 쓰레드에 대한 정보를 담고 있는 Thread 객체가 리턴된다. ThreadLocal 변수에 대한 정보는 이 Thread 객체에 달려있다. Thread 클래스를 열어보면,
ThreadLocal.ThreadLocalMap threadLocals = null;
threadLocals라는 변수가 달려있다. 쓰레드가 코드를 실행하면서 만나게 되는 모든 ThreadLocal 변수들은 여기에 정보가 달리게된다.
ThreadLocalMap 은 최초 16개의 엔트리를 갖는 배열(Entry[])로 선언되며 사용되는 ThreadLocal 변수의 양이 늘어나면 확장된다. ThreadLocalMap의 Key로는 TheadLocal 변수에 대한 WeakReference가 사용되며, GC에 의해 TheadLocal 변수가 회수되면 일련의 동작에 따라 ThreadLocalMap에서도 회수된다.
ThredLocal.get() 메소드는 O(1)의 수행시간이 아닌 O(n) 시간을 소모한다. 여기서 n 은 현재 쓰레드가 사용하고 있는 ThreadLocal 변수의 개수다. 각 쓰레드는 ThreadLocalMap의 Traverse 시작점으로 특정 해시값의 Modular map.length 값을 가지고 있으며 원하는 Key를 찾을 때까지 배열 인덱스를 하나씩 증가시키며 찾는다.
성능이 중요한 코드에서 ThreadLocal을 사용할 때 고려해야할 사항으로 보인다.
ThreadLocal 변수는 정말 Thread 객체에 정보를 달아놔서 쓰레드가 동기화를 원천적으로 필요없게 만들어 놓은 방법이다.
댓글