Double-checked Locking Pattern (DCLP) 을 쓰지 말아야 하는 이유

2020-12-25
  • design-pattern
  • 아래 예시는 이해를 돕기위한 자료로 정확하지 않음. 대충 이런 뜻이구나 정도로 이해하길 바람.

    아래와 같은 싱글톤 패턴에서 SingletonResource 라는 싱글톤 자원을 초기화하고 관리한다고 치자.

    class SingletonResource {
      private int count = -1;
      private SingletonResource() {
        count = 0;
      }
      private SingletonResource singletonResource;
      private static Lock mutex = Lock.init(); //이런 mutex 가 있다고 치자, lock 이 안되면 throw runtime exception
      public ResLock getInstance() {
        if(singletonResource == null) { // 1)
          mutex.lock();
          if(singletonResource == null) { // 2)
            singletonResource = new SingletonResource(); // 3)
          }
        }
        return singletonResource;
      }
      public int getCount() { return count; }
    }
    

    3) 부분은 코드상으로는 한줄로 표현되어있지만, 실제로는

    • 새로운 객체를 위한 메모리를 할당하고
    • 할당된 메모리 주소를 변수에 저장하고
    • 만들어진 객체를 초기화하는 여러 단계를 거친다.

    조건이 하나일 경우에는 여러개의 스레드가 1) 의 첫번째 조건을 다같이 통과 한 것조차 막을 수 없다.

    조건이 두개인 경우에는, 여러개의 스레드가 1) 의 첫번째 조건을 다같이 통과 한 것을 mutex 로 조절할 수 있다.
    적어도 2) 영역의 critical section 에는 하나의 스레드가 접근하는 것을 보장한다.
    하지만, 메모리를 할당하고, 변수에 메모리의 주소를 저장한 것이 실제 메모리에 반영이 되었는지는 보장할 수가 없다.
    따라서, 처음으로 lock 을 획득한 스레드가 변수 초기화를 하는 것에 대한 보장은 되지만, 다음 스레드가 진입했을때, 해당 객체가 null 이 아니라고 해서 초기화 작업을 끝냈을 것이라는 보장이 없는 것이다.

    SingletonResource.getInstance().getCount() == -1 인 경우가 생길 수 있음.

    이럴 때를 위해 volatile 이란 키워드가 있지만, 컴파일러가 어떻게 내부 코드를 재해석하느냐에 따라서 singletonResource 변수에 volatile 키워드를 붙이는 것은 효과가 전혀 없을 수 있다.

    메모리할당, 변수저장, 초기화를 위해 내부적으로 임시변수를 사용하는 경우

    그래서 어쩌라고?

    여러 방식이 있지만, 내가 쓸만한 방법을 두가지만 적어 놓겠다.

    1. Eager Initialization - 이른 초기화

    singleton 객체를 필요할 때가 아니라, class 를 로드하는 시점에 singleton 객체가 생성된다.

    public static class EagerInitializationSingletonResource {
        private static final EagerInitializationSingletonResource eagerInitializationSingletonResource = new EagerInitializationSingletonResource();
        private int count = -1;
        private EagerInitializationSingletonResource() {
            count = 0;
        }
        public static EagerInitializationSingletonResource getInstance() {
            return eagerInitializationSingletonResource;
        }
        public int getCount() { return count; }
    }
    

    2. Initialization-on-demand Holder Idiom - Static Holder 사용

    singleton 객체가 필요할 때(첫 getInstance 메소드가 호출될 때), singleton 객체가 생성된다.

    Bill Pugh Singleton(또는 Static holder singleton pattern, Initialization-on-demand holder idiom) 이라 불리는 방식.
    JVM 의 static initializer 에 의해서 초기화 되는 private static class 의 특성을 이용한 것으로 thread safe는 보장이 되는 원리를 이용한 것이다. synchronized 를 이용하지 않고도 같은 효과를 낼 수 있다.

    public static class StaticHolderSingletonResource {
        private int count = -1;
        private StaticHolderSingletonResource() {
            count = 0;
        }
        public static StaticHolderSingletonResource getInstance() {
            return SingletonHelper.INSTANCE;
        }
        public int getCount() { return count; }
        private static class SingletonHelper {
            private static final StaticHolderSingletonResource INSTANCE = new StaticHolderSingletonResource();
        }
    }
    

    참고