이중 검사 잠 금 실효 분석

이중 검사 잠 금 (이하 DCL) 은 다 중 스 레 드 환경 에서 초기 화 를 지연 시 키 는 효율 적 인 수단 으로 널리 사용 되 고 있다.
안 타 깝 게 도 자바 에 서 는 추가 동기 화가 없 으 면 신뢰 할 수 없다.c + + 와 같은 다른 언어 에서 DCL 을 실현 하려 면 프로세서 의 메모리 모델, 컴 파일 러 가 실행 하 는 재 정렬 과 컴 파일 러 와 동기 화 라 이브 러 리 간 의 상호작용 에 의존 해 야 합 니 다.c + + 가 이런 것들 에 대해 명확 한 규정 을 하지 않 았 기 때문에 DCL 이 효과 가 있 는 지 아 닌 지 는 말 하기 어렵다.DCL 을 적용 하기 위해 c + + 에서 명시 적 메모리 장벽 을 사용 할 수 있 지만 자바 에는 이러한 장벽 이 없습니다.
// Single threaded version
class Foo { 
  private Helper helper = null;
  public Helper getHelper() {
    if (helper == null) 
        helper = new Helper();
    return helper;
    }
  // other functions and members...
}

이 코드 가 다 중 스 레 드 환경 에 사용 되면 오류 가 발생 할 수 있 는 부분 이 몇 개 있 습 니 다.가장 뚜렷 한 것 은 두 개 이상 의 Helper 대상 을 만 들 수 있다 는 것 이다.(뒤에 다른 문제 가 언급 될 것 이다.)getHelper () 방법 을 동기 화하 면 이 문 제 를 복구 할 수 있 습 니 다.
// Correct multithreaded version
class Foo { 
  private Helper helper = null;
  public synchronized Helper getHelper() {
    if (helper == null) 
        helper = new Helper();
    return helper;
    }
  // other functions and members...
}

위의 코드 는 getHelper 를 호출 할 때마다 동기 화 작업 을 수행 합 니 다.DCL 모드 는 helper 대상 이 생 성 된 후에 필요 한 동기 화 를 제거 하 는 데 목적 을 둡 니 다.
// Broken multithreaded version
// "Double-Checked Locking" idiom
class Foo { 
  private Helper helper = null;
  public Helper getHelper() {
    if (helper == null) 
      synchronized(this) {
        if (helper == null) 
          helper = new Helper();
      }    
    return helper;
    }
  // other functions and members...
}

불 행 히 도 이 코드 는 최적화 형 컴 파일 러 에서 든 공유 메모리 프로세서 에서 든 유효 하지 않다.
소 용이 없다
위의 코드 가 작용 하지 않 는 원인 은 매우 많다.다음은 우리 가 먼저 비교적 뚜렷 한 원인 을 몇 가지 말 하 자.이런 것들 을 이해 한 후에 DCL 모델 을 복원 하 는 방법 을 찾 고 싶 을 지도 모른다.당신 의 복구 도 소 용이 없습니다. 이 안 에는 매우 미묘 한 원인 이 있 습 니 다.이런 이 유 를 이해 한 뒤 복 구 를 더 하고 싶 을 지 모 르 지만 더 미묘 한 이유 가 있 기 때문이다.
많은 똑똑 한 사람들 이 이 위 에 많은 시간 을 썼 다.모든 스 레 드 가 helper 대상 에 접근 할 때 잠 금 동작 을 수행 하 는 것 외 에는 방법 이 없습니다.
소 용이 없 는 첫 번 째 원인.
가장 뚜렷 한 이 유 는 Helper 대상 이 초기 화 될 때의 쓰기 동작 과 helper 필드 를 쓰 는 동작 이 무질서 할 수 있 기 때문이다.이렇게 되면 특정한 스 레 드 가 getHelper () 를 호출 하면 helper 필드 가 Helper 대상 을 가리 키 는 것 을 볼 수 있 지만 이 대상 의 필드 값 은 Helper 구조 방법 에 설 치 된 값 이 아 닌 기본 값 입 니 다.
컴 파일 러 가 구조 방법 에 내 연 될 경우 컴 파일 러 가 구조 방법 이 이상 을 던 지 거나 동기 화 작업 을 하지 않 는 다 는 것 을 증명 할 수 있다 면 대상 의 쓰기 작업 과 hepler 필드 의 쓰기 작업 을 초기 화 하 는 것 사이 에 자 유 롭 게 정렬 할 수 있 습 니 다.
컴 파일 러 가 이 쓰기 동작 을 다시 정렬 하지 않 더 라 도 다 중 프로세서 나 메모리 시스템 에서 이 쓰기 동작 을 다시 정렬 할 수 있 습 니 다. 다른 프로세서 에서 실행 되 는 스 레 드 는 정렬 이 가 져 온 결 과 를 볼 수 있 습 니 다.
Doug Lea 는 컴 파일 러 의 정렬 에 관 한 더 자세 한 글 을 썼 다.
소 용이 없 는 테스트 사례 를 보 여 주다.
Paul Jakubik 은 DCL 을 사용 해 제대로 작 동 하지 못 하 는 예 를 찾 았 다.아래 의 코드 는 약간의 정 리 를 했다.
public class DoubleCheckTest
{


  // static data to aid in creating N singletons
  static final Object dummyObject = new Object(); // for reference init
  static final int A_VALUE = 256; // value to initialize 'a' to
  static final int B_VALUE = 512; // value to initialize 'b' to
  static final int C_VALUE = 1024;
  static ObjectHolder[] singletons;  // array of static references
  static Thread[] threads; // array of racing threads
  static int threadCount; // number of threads to create
  static int singletonCount; // number of singletons to create


  static volatile int recentSingleton;


  // I am going to set a couple of threads racing,
  // trying to create N singletons. Basically the
  // race is to initialize a single array of 
  // singleton references. The threads will use
  // double checked locking to control who 
  // initializes what. Any thread that does not
  // initialize a particular singleton will check 
  // to see if it sees a partially initialized view.
  // To keep from getting accidental synchronization,
  // each singleton is stored in an ObjectHolder 
  // and the ObjectHolder is used for 
  // synchronization. In the end the structure
  // is not exactly a singleton, but should be a
  // close enough approximation.
  // 


  // This class contains data and simulates a 
  // singleton. The static reference is stored in
  // a static array in DoubleCheckFail.
  static class Singleton
    {
    public int a;
    public int b;
    public int c;
    public Object dummy;

    public Singleton()
      {
      a = A_VALUE;
      b = B_VALUE;
      c = C_VALUE;
      dummy = dummyObject;
      }
    }

  static void checkSingleton(Singleton s, int index)
    {
    int s_a = s.a;
    int s_b = s.b;
    int s_c = s.c;
    Object s_d = s.dummy;
    if(s_a != A_VALUE)
      System.out.println("[" + index + "] Singleton.a not initialized " +
s_a);
    if(s_b != B_VALUE)
      System.out.println("[" + index 
                         + "] Singleton.b not intialized " + s_b);

    if(s_c != C_VALUE)
      System.out.println("[" + index 
                         + "] Singleton.c not intialized " + s_c);

    if(s_d != dummyObject)
      if(s_d == null)
        System.out.println("[" + index 
                           + "] Singleton.dummy not initialized," 
                           + " value is null");
      else
        System.out.println("[" + index 
                           + "] Singleton.dummy not initialized," 
                           + " value is garbage");
    }

  // Holder used for synchronization of 
  // singleton initialization. 
  static class ObjectHolder
    {
    public Singleton reference;
    }

  static class TestThread implements Runnable
    {
    public void run()
      {
      for(int i = 0; i < singletonCount; ++i)
        {
    ObjectHolder o = singletons[i];
        if(o.reference == null)
          {
          synchronized(o)
            {
            if (o.reference == null) {
              o.reference = new Singleton();
          recentSingleton = i;
          }
            // shouldn't have to check singelton here
            // mutex should provide consistent view
            }
          }
        else {
          checkSingleton(o.reference, i);
      int j = recentSingleton-1;
      if (j > i) i = j;
      }
        } 
      }
    }

  public static void main(String[] args)
    {
    if( args.length != 2 )
      {
      System.err.println("usage: java DoubleCheckFail" +
                         " 
    
    
     ");
      }
    // read values from args
    threadCount = Integer.parseInt(args[0]);
    singletonCount = Integer.parseInt(args[1]);

    // create arrays
    threads = new Thread[threadCount];
    singletons = new ObjectHolder[singletonCount];

    // fill singleton array
    for(int i = 0; i < singletonCount; ++i)
      singletons[i] = new ObjectHolder();

    // fill thread array
    for(int i = 0; i < threadCount; ++i)
      threads[i] = new Thread( new TestThread() );

    // start threads
    for(int i = 0; i < threadCount; ++i)
      threads[i].start();

    // wait for threads to finish
    for(int i = 0; i < threadCount; ++i)
      {
      try
        {
        System.out.println("waiting to join " + i);
        threads[i].join();
        }
      catch(InterruptedException ex)
        {
        System.out.println("interrupted");
        }
      }
    System.out.println("done");
    }
}

    
   

위 코드 가 Symantec JIT 를 사용 하 는 시스템 에서 실 행 될 때 정상적으로 작 동 하지 않 습 니 다.특히 시 만 텍 JIT 는
singletons[i].reference = new Singleton();

다음 모양 으로 컴 파일 되 었 습 니 다.
0206106A   mov         eax,0F97E78h
0206106F   call        01F6B210                  ; allocate space for
                                                 ; Singleton, return result in eax
02061074   mov         dword ptr [ebp],eax       ; EBP is &singletons[i].reference 
                                                ; store the unconstructed object here.
02061077   mov         ecx,dword ptr [eax]       ; dereference the handle to
                                                 ; get the raw pointer
02061079   mov         dword ptr [ecx],100h      ; Next 4 lines are
0206107F   mov         dword ptr [ecx+4],200h    ; Singleton's inlined constructor
02061086   mov         dword ptr [ecx+8],400h
0206108D   mov         dword ptr [ecx+0Ch],0F84030h

보시 다시 피 singletons [i]. reference 에 할당 하 는 작업 은 Singleton 구조 방법 전에 했 습 니 다.기 존의 자바 메모리 모델 에서 이것 은 완전히 허용 되 고 c 와 c + 에서 도 합 법 적 입 니 다. (c / c + + 메모리 모델 이 없 기 때 문 입 니 다.
소 용이 없 는 복구
앞에서 설명 한 원인 을 바탕 으로 일부 사람들 은 다음 과 같은 코드 를 제시 했다.
// (Still) Broken multithreaded version
// "Double-Checked Locking" idiom
class Foo { 
  private Helper helper = null;
  public Helper getHelper() {
    if (helper == null) {
      Helper h;
      synchronized(this) {
        h = helper;
        if (h == null) 
            synchronized (this) {
              h = new Helper();
            } // release inner synchronization lock
        helper = h;
        } 
      }    
    return helper;
    }
  // other functions and members...
}

Helper 대상 을 만 드 는 코드 를 내부 동기 화 블록 에 넣 었 습 니 다.직관 적 인 생각 은 동기 블록 을 종료 할 때 메모리 장벽 이 있어 야 한 다 는 것 이다. 이것 은 Helper 의 초기 화 와 helper 필드 할당 사이 의 정렬 을 막 을 수 있다.
불행 하 게 도 이런 직감 은 완전히 틀 렸 다.동기 화 규칙 은 그렇지 않다.Monitorexit (즉, 동기 블록 을 종료 하 는 것) 의 규칙 은 Monitorexit 앞의 action 이 이 Monitor 가 풀 리 기 전에 실행 해 야 한 다 는 것 입 니 다.그러나 Monitorexit 뒤의 action 은 Monitorexit 가 풀 리 기 전에 실행 할 수 없다 는 규정 은 없다.따라서 컴 파일 러 는 helper = h 를 할당 합 니 다.동기 블록 으로 옮 기 는 것 은 매우 합리적이다. 이것 은 우리 가 전에 말 한 문제 로 돌아 갔다.많은 프로세서 들 이 이러한 단 방향 메모리 장벽 명령 을 제공 했다.잠 금 방출 의 의 미 를 바 꾸 면 방출 시 양 방향 메모리 장벽 을 실행 하면 성능 손실 을 가 져 올 수 있 습 니 다.
더 많은 소 용이 없 는 '복구'
쓰기 동작 을 할 때 양 방향 메모리 장벽 을 실행 하도록 할 수 있 는 일 을 할 수 있다.이것 은 매우 중량급 과 비효 율 적 이 며 자바 메모리 모델 이 수정 되면 제대로 작 동 하지 못 할 것 이 거의 확실 하 다.그 러 지 마.만약 이것 에 관심 이 있다 면, 나 는 다른 홈 페이지 에서 이런 기술 을 묘사 했다.그것 을 사용 하지 마라.
그러나 helper 대상 의 스 레 드 를 초기 화하 더 라 도 양 방향 메모리 장벽 을 사용 하 더 라 도 소 용이 없습니다.
문 제 는 일부 시스템 에서 helper 필드 가 null 이 아 닌 스 레 드 를 보 더 라 도 메모리 장벽 을 실행 해 야 한 다 는 점 이다.
왜?프로세서 에 메모리 캐 시 복사 가 있 기 때 문 입 니 다.일부 프로세서 에 서 는 프로세서 가 cache coherence 명령 (즉, 메모리 장벽) 을 실행 하지 않 는 한 읽 기 동작 은 만 료 된 로 컬 캐 시 복사 에서 값 을 가 져 올 수 있 습 니 다. 다른 프로세서 가 메모리 장벽 을 사용 하 더 라 도 쓰기 동작 을 메모리 에 다시 쓸 수 있 습 니 다.
나 는 알파 프로세서 에서 어떻게 발생 했 는 지 에 대해 다른 페이지 를 열 었 다.
이렇게 큰 힘 을 쓸 만 합 니까?
대부분의 응용 프로그램 에서 getHelper () 를 동기 화 방법 으로 바 꾸 는 대 가 는 높 지 않다.이것 이 확실히 많은 응용 비용 을 초래 했다 는 것 을 알 아야 만 이런 세부 적 인 최적화 를 고려 해 야 한다.
일반적으로 더 높 은 등급 의 기술, 예 를 들 어 내부 의 병합 정렬 을 사용 하 는 것 은 정렬 을 교환 하 는 것 이 아니 라 (SPECJVM DB 의 기준 참조) 가 가 져 오 는 영향 이 더욱 크다.
정적 단일 예 적용
static 단일 대상 (즉, Helper 대상 만 만 들 수 있 습 니 다) 을 만 들 려 면 간단 하고 우아 한 해결 방안 이 있 습 니 다.
singleton 변 수 를 다른 종류의 정적 필드 로 만 사용 합 니 다.자바 의 의 미 는 이 필드 가 인용 되 기 전에 초기 화 되 지 않 고 이 필드 에 접근 하 는 모든 스 레 드 를 볼 수 있 도록 합 니 다.
class HelperSingleton {
    static Helper singleton = new Helper();
}

32 비트 의 기본 유형 변수 DCL 에 유효 합 니 다.
DCL 모드 는 대상 참조 에 사용 할 수 없 지만 32 비트 의 기본 형식 변수 에 사용 할 수 있 습 니 다.DCL 도 log 와 double 유형의 기본 변수 에 사용 할 수 없습니다. 동기 화 되 지 않 은 64 비트 기본 변수의 읽 기와 쓰기 가 원자 조작 이라는 것 을 보장 할 수 없 기 때 문 입 니 다.
// Correct Double-Checked Locking for 32-bit primitives
class Foo { 
  private int cachedHashCode = 0;
  public int hashCode() {
    int h = cachedHashCode;
    if (h == 0) 
    synchronized(this) {
      if (cachedHashCode != 0) return cachedHashCode;
      h = computeHashCode();
      cachedHashCode = h;
      }
    return h;
    }
  // other functions and members...
}

사실 컴퓨터 HashCode 방법 이 항상 같은 결 과 를 되 돌려 주 고 다른 부속 작용 이 없 을 때 (즉, 컴퓨터 HashCode 는 멱 등 방법), 심지어 이곳 의 모든 동기 화 를 없 앨 수 있다.
// Lazy initialization 32-bit primitives
// Thread-safe if computeHashCode is idempotent
class Foo { 
  private int cachedHashCode = 0;
  public int hashCode() {
    int h = cachedHashCode;
    if (h == 0) {
      h = computeHashCode();
      cachedHashCode = h;
      }
    return h;
    }
  // other functions and members...
}

명시 적 메모리 장벽 으로 DCL 을 유효 하 게 하 다.
명시 적 메모리 장벽 명령 이 사용 가능 하 다 면 DCL 이 적 용 될 수 있 습 니 다.예 를 들 어 C + + 를 사용한다 면 Doug Schmidt 등 이 저술 한 책의 코드 를 참고 할 수 있 습 니 다.
// C++ implementation with explicit memory barriers
// Should work on any platform, including DEC Alphas
// From "Patterns for Concurrent and Distributed Objects",
// by Doug Schmidt
template 
   
     TYPE *
Singleton
    
     ::instance (void) {
    // First check
    TYPE* tmp = instance_;
    // Insert the CPU-specific memory barrier instruction
    // to synchronize the cache lines on multi-processor.
    asm ("memoryBarrier");
    if (tmp == 0) {
        // Ensure serialization (guard
        // constructor acquires lock_).
        Guard
     
       guard (lock_);
        // Double check.
        tmp = instance_;
        if (tmp == 0) {
                tmp = new TYPE;
                // Insert the CPU-specific memory barrier instruction
                // to synchronize the cache lines on multi-processor.
                asm ("memoryBarrier");
                instance_ = tmp;
        }
    return tmp;
}

     
    
   

DCL 을 스 레 드 부분 저장 소 로 복구 합 니 다.
Alexander Terekhov ([email protected]) DCL 을 실현 할 수 있 는 교묘 한 방법 인 스 레 드 부분 저장 소 를 사용 하 는 것 을 제시 했다.각 스 레 드 는 동기 화 를 실 행 했 는 지 여 부 를 표시 하기 위해 각각 flag 를 저장 합 니 다.
class Foo {
 /** If perThreadInstance.get() returns a non-null value, this thread
    has done synchronization needed to see initialization
    of helper */
     private final ThreadLocal perThreadInstance = new ThreadLocal();
     private Helper helper = null;
     public Helper getHelper() {
         if (perThreadInstance.get() == null) createHelper();
         return helper;
     }
     private final void createHelper() {
         synchronized(this) {
             if (helper == null)
                 helper = new Helper();
         }
     // Any non-null value would do as the argument here
         perThreadInstance.set(perThreadInstance);
     }
}

이런 방식 의 성능 은 사용 하 는 JDK 구현 에 크게 의존한다.Sun 1.2 구현 에서 ThreadLocal 은 매우 느리다.1.3 에서 더 빨 라 져 1.4 에서 한 단계 더 올 라 갈 수 있 기 를 기대한다.Doug Lea 는 초기 화 기술 의 실현 지연 성능 을 분석 했다.
새로운 자바 메모리 모델 에서
JDK 5 는 새로운 자바 메모리 모델 과 스 레 드 규범 을 사용 했다.
volatile 로 DCL 복구
JDK 5 와 후속 버 전 은 volatile 의 미 를 확 장 했 습 니 다. volatile 쓰기 동작 과 앞의 읽 기와 쓰기 동작 을 다시 정렬 하 는 것 을 허용 하지 않 고 volatile 읽 기 동작 과 뒤의 읽 기와 쓰기 동작 을 다시 정렬 하 는 것 도 허용 하지 않 습 니 다.더 자세 한 정 보 는 Jeremy Manson 의 블 로그 참조.
// Works with acquire/release semantics for volatile
// Broken under current semantics for volatile
class Foo {
    private volatile Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            synchronized(this) {
                if (helper == null)
                    helper = new Helper();
            }
        }
        return helper;
    }
}

가 변 대상 DCL
Helper 가 가 변 대상 이 라면 Helper 의 모든 필드 가 final 이면 volatile 을 사용 하지 않 아 도 DCL 이 유효 합 니 다.주로 가 변 적 이지 않 은 대상 을 가리 키 는 인용 은 int 와 float 와 같은 행 위 를 나타 내야 하기 때문이다.읽 기와 쓰기 불가 변 대상 의 인용 은 원자 조작 이다.
원문 Double Checked Locking 번역 정 일 via ifeve

좋은 웹페이지 즐겨찾기