Double-Checked Locking

최진규·2023년 9월 30일
0

Design Patterns

목록 보기
2/2
  • 소프트웨어 디자인 패턴
  • 락 획득 이전에 locking criterion(lock hint)을 사전 테스트하여 락획득의 오버헤드를 줄이는 방법이다.
  • locking criterion check가 locking이 필요하다고 판단내리는 경우에만 locking이 발생한다.

motivation and original pattern

single-thread version

public class DoubleCheckedLocking {
  
  private static ThreadRaceTarget target;
  public ThreadRaceTarget getTarget() {
    if (target == null) {
      target = new ThreadRaceTarget();
    }
    return target;
  }
}

single thread의 경우에는 괜찮지만 이 경우에는 멀티스레드 환경에서는 정상적으로 동작하지 않는다.
만약에 두개의 thread에서 getTarget()을 호출한다면, 의도한 바와는 다르게 생성자는 두번 호출될것이다.

따라서 이런 경우에는 의도한 바대로 정상동작하게끔 하기 위해서 lock을 잡아야 하는데,
synchronized를 사용해보자.

multi-thread version

public class DoubleCheckedLocking {

  private ThreadRaceTarget target;
  public synchronized ThreadRaceTarget getTarget() {
    if (target == null) {
      target = new ThreadRaceTarget();
    }
    return target;
  }
}

이 코드는 의도한대로 정상 동작한다.
예를 들어서 두개의 thread에서 하나의 DoubleCheckedLocking instance에 접근해서 getTarget()을 호출한다면,
synchronized가 인스턴스 단위로 걸려있기 때문에 락이 걸려있어 동시성 방어가 된다.

하지만 성능은 어떨까 ?
모든 thread에서 락을 얻고 release하는 과정이 반복되게 된다면 이는 성능을 저하시킬 수도 있다.
왜냐하면 락을 거는 이유는 getTarget()이 호출되었을때, 초기화가 되지 않은 경우가 중복 수행되는 방지하기 위해서이다.
만약 초기화가 완료된 시점에서는 락을 거는건 더이상의 효용이 없다.
그저 멤버 변수를 바로 가져오면 끝날일이다.

따라서 다음과 같은 방법으로 최적화가 가능하다.

double checked locking

public class DoubleCheckedLocking {

  private ThreadRaceTarget target;
  public ThreadRaceTarget getTarget() {
    if (target == null) {
      synchronized (this) {
        if (target == null) {
          target = new ThreadRaceTarget();
        }
      }
    }
    return target;
  }
}
  1. target이 초기화되었는지 먼저 체크한다.
  2. target이 초기화되지 않았다면 락을 걸고, 초기화를 진행하고 락을 해제한다.
  3. target이 초기화되었다면 바로 target을 반환한다.

이는 앞의 방법들과는 성능적으로 최적화 되어 있다.
예를 들어서 100개의 thread가 race하는 상황을 가정해보면,

이전 구현에서는 100번 락을 얻고, 락을 해제한다.
새로운 구현에서는 1번만 락을 얻고, 락을 해제한다.

이 패턴을 사용할때 주의하지 않으면 데이터 경합이 발생한다.

  1. Thread A가 target이 초기화되지 않음을 알고 락을 걸고 초기화한다.
  2. 특정 언어 컴파일러는 초기화가 완료되지 않아도 부분 생성된 객체를 제공한다. 따라서 Thread B는 target이 Thread A에 의해서 생성이 완료되지 않았음에도 불구하고 접근이 가능하게 된다.
  3. 이는 불완전한 데이터를 Thread B가 갖음으로써 crash가 발생할 여지가 있다.

따라서 다음과 같은 방법으로 사용해야 한다.

volatile double checked locking


public class DoubleCheckedLocking {

  private volatile ThreadRaceTarget target;
  
  public ThreadRaceTarget getTarget() {
    ThreadRaceTarget temp = target;
    if (temp == null) {
      synchronized (this) {
        temp = target;
        if (temp == null) {
          target = temp =  new ThreadRaceTarget();
        }
      }
    }
    return temp;
  }
}

volatile은 memory barrier를 제공하기 때문에 위의 초기화가 완료되고 나서 target에 접근한다.

여기서 local 변수인 temp는 불필요해 보이지만, 이를 도입함으로써 인해 volatile 변수인 target은 getTarget() 내부에서 한번만 access된다.
이 자체가 성능을 약 40% 정도 향상시킨다.

profile
개발하는 개복치

0개의 댓글

관련 채용 정보