본문 바로가기

개발 개발/자바

ThreadLocal 예제개발 이야기

출처 : http://0thinktank.tistory.com/entry/ThreadLocal-%EC%98%88%EC%A0%9C

특정 쓰레드의 스코프에서 사용할 로컬변수가 필요했던 적이 있는가? 이때에 각각의 쓰레드는 고유의 스토리지를 갖고 하나의 쓰레드는 다른 쓰레드의 상태 정보를 액세스하는 것이 불가능할 것이다. 표준 라이브러리는 이러한 요구를 가능케 하는 ThreadLocalInheritableThreadLocal, 2개의 클래스들을 제공하고 있다.

클래스들이 사용되고 있는 예를 보자.

  import java.util.Random;

  public class ThreadLocal1 {

    // Define/create thread local variable
    static ThreadLocal threadLocal = 
                                   new ThreadLocal();
    // Create class variable
    static volatile int counter = 0;
    // For random number generation
    static Random random = new Random();

    // Displays thread local variable, counter, 
    // and thread name
    private static void displayValues() {
      System.out.println (
        threadLocal.get() + "\t" + counter +
        "\t" + Thread.currentThread().getName());
    }

    public static void main (String args[]) {

      // Each thread increments counter
      // Displays variable info
      // And sleeps for the random amount of time
      // Before displaying info again
      Runnable runner = new Runnable() {
        public void run() {
          synchronized(ThreadLocal1.class) {
            counter++;
          }
          threadLocal.set(
             new Integer(random.nextInt(1000)));
          displayValues();
          try {
            Thread.currentThread().sleep (
              ((Integer)threadLocal.get()).intValue());
            displayValues();
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      };

      // Increment counter, access thread local from 
      // a different thread, and display the values
      synchronized(ThreadLocal1.class) {
          counter++;
      }
      threadLocal.set(
         new Integer(random.nextInt(1000)));
      displayValues();

      // Here's where the other threads
      //  are actually created
      for (int i=0; i<5; i++) {
        Thread t = new Thread(runner);
        t.start();
      }
    }
  }

ThreadLocal1클래스는 2개의 정적 변수를 갖는다. common integer primitive 타입인 counterThreadLocal 타입의 변수인 threadLocal이 바로 그것이다. 일반적으로 클래스 변수를 사용하면, 해당 클래스의 모든 인스턴스들은 동일한 값을 공유하게 된다. 하지만 ThreadLocal의 경우는 다르다. ThreadLocal클래스를 사용하면 동일한 쓰레드내에서만 같은 정적 정보를 공유하게 되는 반면, 서로 다른 쓰레드에서 ThreadLocal 클래스를 사용하면 서로 다른 정적 정보를 갖게 된다.

ThreadLocal1에서, ThreadLocal 인스턴스는 0에서 999사이의 랜덤한 숫자로 초기화된다.

   threadLocal.set(
     new Integer(random.nextInt(1000)));

다수의 서로 다른 쓰레드가 생성되었다. 각각의 쓰레드 별로, 2개의 클래스 변수에 대한 상태 정보가 디스플레이된다. 그러면 이 쓰레드는 ThreadLocal이 랜덤한 숫자로 지정한 만큼의 밀리초동안 휴면한다. 이후, 2개의 클래스 변수의 상태 정보가 다시 디스플레이되고, ThreadLocal1를 실행하면, 아래와 같은 출력값을 보게 된다.

   828     1       main
   371     2       Thread-0
   744     3       Thread-1
   734     4       Thread-2
   189     5       Thread-3
   790     6       Thread-4
   189     6       Thread-3
   371     6       Thread-0
   734     6       Thread-2
   744     6       Thread-1
   790     6       Thread-4

여기에서 주목할 것은 세번째 컬럼에서 쓰레드의 이름이 같을 때를 살펴보면, 첫번째 컬럼의 ThreadLocal 변수값이 프로그램이 실행되는 동안 같은 상태를 유지한다는 것이다. 반면, 두번째 컬럼에서 보이는 정적 변수는 프로그램이 실행하는 동안 변화하고, 기존 쓰레드를 재검토할 때 그 바뀐 값을 유지한다. 이것이 바로 적절하게 동기화된 정적 변수가 해야 하는 일이다.

프로그램을 실행할 때마다 모든 컬럼에는 각기 다른 값이 생성되기 때문에 결과에서 보이는 첫번째 컬럼의 시간들이 위에서 보는 바와 다르다거나, 마지막 5개의 행의 이름의 순서가 다르다고 해도 놀랄 필요는 없다. 그 순서는 첫번째 컬럼에서 보여진 시간(times)에 기반한 것이다.

쓰레드 로컬 변수는 단순히 인스턴스 변수들의 또 다른 형태만이 아니다. 만약 특정 클래스의 10개의 인스턴스를 모두 일반적인 쓰래드내에서 접근한다면, 그것들의 ThreadLocal 정보는 모두 같을 것이다. 그러나, 만약 이러한 인스턴스들 중의 하나를 다른 쓰레드에서 접근하게 되면, ThreadLocal 정보는 바뀌게 될 것이다.

ThreadLocal 클래스는 오직 3개의 메소드만을 포함하고 있는데 get, setinitialValue가 바로 그것이다. ThreadLocal1 예제를 보면, set메소드는 인스턴스가 생성된 이후 호출되는 것을 알 수 있다. ThreadLocal의 상태를 이후에 다시 초기화하는 방법 대신에, ThreadLocal을 상속한 서브클래스에서 initialValue 메소드를 오버라이드할 수 있다. 다음의 ThreadLocal2 클래스가 바로 그 일을 하는데, 생성된 출력값은 ThreadLocal1의 경우와 비슷하다.

   import java.util.Random;

   public class ThreadLocal2 {

     // Create thread local class
     // Initial value is a random number from 0-999
     private static class MyThreadLocal
                                  extends ThreadLocal {
       Random random = new Random();
       protected Object initialValue() {
         return new Integer(random.nextInt(1000));
       }
     }

     // Define/create thread local variable
     static ThreadLocal threadLocal =
                           new MyThreadLocal();

     // Create class variable
     static volatile int counter = 0;

     // For random number generation
     static Random random = new Random();

     // Displays thread local variable, counter,
     // and thread name
     private static void displayValues() {
       System.out.println (
         threadLocal.get() + "\t" + counter +
         "\t" + Thread.currentThread().getName());
     }
    
     public static void main (String args[]) {

       // Each thread increments counter
       // Displays variable info
       // And sleeps for the random amount of time
       // Before displaying info again
       Runnable runner = new Runnable() {
         public void run() {
           synchronized(ThreadLocal2.class) {
             counter++;
           }
           displayValues();
           try {
             Thread.currentThread().sleep (
               ((Integer)threadLocal.get()).intValue());
             displayValues();
           } catch (InterruptedException e) {
             e.printStackTrace();
           }
         }
       };

       // Another instance of class created
       // and values displayed
       displayValues();

       // Here's where the other threads
       // are actually created
       for (int i=0; i<5; i++) {
         Thread t = new Thread(runner);
         t.start();
       }
     }
   } 

ThreadLocalinitialValue 메소드는 각각의 쓰레드가 상태를 알아내기 위해 하는 첫번째 호출에서 사용된다. Set이 이미 호출된 상태라면, initialValue 메소드는 절대로 호출되지 않는다. initialValue 메소드를 사용할 때, public이 아니라 protected로 만들어야 한다는 것을 기억하자. 일반적으로 initialValue 메소드가 임의로 호출되는 것은 바람직하지 않기 때문이다. initialValue 가 오버라이드되지 않았을 경우, 쓰레드 로컬 변수를 나타내는 객체의 초기 값은 null이다.

ThreadLocal변수에 담을 컨텐츠는 어떤 Object 타입이든지 가능하다. ThreadLocal2에서 컨텐츠는 Integer이다. get을 이용해서 상태를 받아낼 때 Object를 적절한 타입으로 캐스팅해야 한다는 것을 기억하자.

InheritableThreadLocal라고 불리는 ThreadLocal의 두번째 타입을 보자. ThreadLocal과 같이, InheritableThreadLocal을 사용하는 각각의 쓰레드는 고유의 상태객체를 갖는다. ThreadLocalInheritableThreadLocal의 차이점은 InheritableThreadLocal의 초기값은 생성하는 쓰레드로부터 변수의 상태를 얻어낸다는 데에 있다. 상태의 변경은 다른 쓰레드를 제외한 로컬 쓰레드에서만 가능하다. InheritableThreadLocal의 새로운 childValue 메소드는 그것의 상태를 초기화하기위해 사용되는데 이는 상태를 얻어내려고 할 때가 아니라 자녀객체의 생성자에 의해 호출된다.

실제로 ThreadLocal의 사용을 검토하기 위해서는, java.nio.charsetCharset 클래스를 보거나 java.util.LoggingLogRecord를 참고한다. Charset의 경우에는, ThreadLocal가 캐릭터 세트 인코더와 디코더를 검색하기 위해 사용된다. 이것은 초기화하는 코드들이 절절한 시기에 사용되도록 조절하는 역할을 한다. LogRecord에서 ThreadLocal의 사용은 각각의 쓰레드에 Login을 위한 고유한 id가 할당되는 것을 보장해 준다.