이중 검사 잠 금 실효 분석
안 타 깝 게 도 자바 에 서 는 추가 동기 화가 없 으 면 신뢰 할 수 없다.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
이 내용에 흥미가 있습니까?
현재 기사가 여러분의 문제를 해결하지 못하는 경우 AI 엔진은 머신러닝 분석(스마트 모델이 방금 만들어져 부정확한 경우가 있을 수 있음)을 통해 가장 유사한 기사를 추천합니다:
【eclipse】같은 파일을 2개 열고 싶다【에디터의 분할】「이런 것은 다른 클래스로 나누어야 한다!」라든지 있다고는 생각합니다만. 실제로 실무 속에서 프로그램을 쓰고 있으면, 이런 소스에 눈에 걸리는 일도 적지 않을까···. 그건 그렇고, 내 노트북에서 이렇게 보입니다 네...
텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
CC BY-SA 2.5, CC BY-SA 3.0 및 CC BY-SA 4.0에 따라 라이센스가 부여됩니다.