자바를 비롯한 다양한 언어에서 멀티 쓰레드 프로그래밍을 지원한다. 소스코드의 다양한 부분을 멀티 쓰레드로 수행하면 많은 장점들을 얻을 수 있다. 하지만 멀티 쓰레드 프로그램을 작성할 때 가장 고민해야 하는 부분이 있으니 바로 공유 리소스로의 접근제어다. 그 중에 특정 변수 값을 세팅하고 읽는 과정을 동기화해야 하는 경우를 가장 많이 만나게 된다.
synchronized 키워드를 이용하여 특정 메소드 혹은 코드 블록을 동기화 시키는 방법은 잘 알고 있을 것이다. Synchronized 키워드와 더불어 자바에서 제공하는 멀티 쓰레드를 위한 도구로 ThreadLocal이 있다. ThreadLocal은 각 쓰레드가 자신만의 전역변수를 만들어 사용할 수 있는 기능을 제공해준다.
우선 코드를 살펴보자.
이 코드를 실행하면 다음 결과를 얻게 된다.
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
소스코드를 설명해보자면, ThreadLocal 객체 변수를 private static으로 선언하여 여러 쓰레드에서 접근할 수 있도록 만들었다. 그리고 각 쓰레드가 실행되는 run() 메소드 안에서 myInfo.get()을 통해 각자 자신(Thread)에게 할당된 myInfo 객체를 얻어오고, myInfo.set() 메소드를 이용해서 자신의 myInfo 값을 새로 세팅, 이후 다시 myInfo.get() 메소드를 호출하여 세팅된 값을 확인한다.
코드에서는 synchronized를 사용하지 않았기 때문에 사용자 레벨에서 추가한 동기화 메카니즘은 없다. 하지만 실행결과를 보자면 서로 다른 쓰레드에게 간섭을 일으키지 않았음을 알 수 있다. 쓰레드 이름을 의미하는 숫자와 newValue가 모두 동일하며 이는 다른 쓰레드에 의해서 간섭받지 않았음을 의미한다.
각 쓰레드는 공통적으로 private static ThreadLocal myInfo 변수에 접근하여 get(), set() 메소드를 사용한다. 이 경우 각 쓰레드들은 자신에게 할당된 myInfo 정보에 접근하게 되므로 다른 쓰레드의 myInfo 정보에 간섭을 할 수가 없다. 따라서 별도의 동기화가 없어도 서로 다른 쓰레드의 간섭없이 myInfo를 사용할 수 있게 된다.
initialValue() 메소드
우선 테스트 코드에서 살펴볼 사항으로 initialValue() 메소드가 있다. 이 메소드를 오버라이드(Override) 했는데 원래는 return null; 을 하는 메소드다. 이 메소드는 ThreadLocal 객체인 myInfo 객체의 myInfo.get() 메소드를 호출 할 때, 설정된 값이 없는 경우 기본 값을 설정하기 위해서 호출되는 메소드다.
기본 메소드는 null을 리턴하지만 테스트 코드에서는 "defaultName"을 리턴하도록 오버라이드 했다.
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 객체에 정보를 달아놔서 쓰레드가 동기화를 원천적으로 필요없게 만들어 놓은 방법이다.
댓글