자바 의 synchronized 와 volatile 키워드

원문의 출처:http://hukai.me/android-training-course-in-chinese/performance/smp/index.html
자바 의 "synchronized" 와 "volatile" 키워드
'synchronized' 키 워드 는 자바 에 내 장 된 잠 금 체 제 를 제공 합 니 다.모든 대상 에 대응 하 는 'Monitor' 가 있 는데 이 감청 기 는 서로 배척 하 는 접근 을 제공 할 수 있다.
'synchronized' 코드 세그먼트 의 실현 체 제 는 자 회전 자물쇠 (spin lock) 와 같은 기본 구 조 를 가지 고 있 습 니 다. 그들 은 모두 CAS 를 얻 는 것 부터 시작 하여 CAS 를 방출 하 는 것 으로 끝 납 니 다.이 는 컴 파일 러 (copilers) 와 코드 최적화 기 (code optimizers) 가 코드 를 'synchronized' 코드 세그먼트 로 쉽게 옮 길 수 있다 는 뜻 이다.하나의 실천 결 과 는 synchronized 코드 세그먼트 가 이 코드 아래 부분 에서 실행 되 는 지, 아니면 이 코드 위의 일부분 뒤에 실행 되 는 지 판단 할 수 없습니다.더 나 아가 한 방법 에 두 개의 synchronized 코드 세그먼트 가 있 고 같은 대상 이 잠 겨 있다 면 이 두 작업 의 중간 코드 는 다른 스 레 드 에 의 해 감지 되 지 않 고 컴 파일 러 는 '잠 금 조 화 된 lock coarsening' 을 실행 하고 이 두 가 지 를 같은 코드 블록 에 묶 을 수 있 습 니 다.
또 다른 관련 키 워드 는 'volatile' 이다.자바 1.4 와 이전 문서 에서 이렇게 정의 되 었 습 니 다. volatile 성명 은 해당 하 는 C 언어 와 같 습 니 다.자바 1.5 부터 더욱 강력 한 보장 을 제공 하고 심지어 synchronization 과 마찬가지 로 강력 한 동기 화 체 제 를 갖 추고 있다.
volatile 의 접근 효 과 는 다음 과 같은 예 로 설명 할 수 있 습 니 다.스 레 드 1 이 volatile 필드 에 할당 작업 을 했다 면 스 레 드 2 는 그 필드 의 값 을 읽 었 습 니 다. 스 레 드 2 는 이전 스 레 드 1 의 모든 쓰기 동작 을 볼 수 있 도록 확보 되 었 습 니 다.더 일반적인 상황 은 모든 스 레 드 가 그 필드 에 대한 쓰기 작업 이 스 레 드 2 에 있어 서 볼 수 있다 는 것 이다.실제로 volatile 을 쓰 는 것 은 모니터 를 풀 어 주 는 것 과 같 고 volatile 을 읽 는 것 은 모니터 를 가 져 오 는 것 과 같다.
비 volatile 의 방문 은 volatile 의 방문 을 배려 하기 때문에 순서 조정 이 필요 할 수 있 습 니 다.예 를 들 어 컴 파일 러 는 비 volatile 로 딩 작업 을 위로 이동 할 수 있 지만 아래로 이동 하지 않 습 니 다.Volatile 간 의 방문 은 서로 때문에 순 서 를 조정 하지 않 습 니 다.가상 기 회 는 메모리 울타리 (memory barriers) 를 처리 하 는 방법 에 주의 하 십시오.
대부분의 기본 데이터 형식 을 불 러 오고 저장 할 때 그들 은 원자의 atomic 이 고 log 와 double 형식의 데 이 터 는 volatile 로 밝 혀 지지 않 는 한 원자 형 을 갖 추 지 않 습 니 다.단일 핵 처리 장치 에서 도 다 중 스 레 드 업데이트 비 volatile 필드 값 은 확실 하지 않 습 니 다.
Examples
다음은 잘못된 단 보 카운터 (monotonic counter) 의 예 입 니 다. (Java theory and practice: Managing volatility).
class Counter {
    private int mValue;

    public int get() {
        return mValue;
    }
    public void incr() {
        mValue++;
    }
}

get () 과 incr () 방법 이 다 중 스 레 드 로 호출 된다 고 가정 합 니 다.그리고 get () 방법 이 호출 될 때 모든 스 레 드 가 현재 의 수량 을 볼 수 있 도록 확보 하고 싶 습 니 다.가장 눈길 을 끄 는 문 제 는 mValue + + 가 실제로 다음 세 가지 조작 을 포함 하고 있다 는 점 이다.
reg = mValue
reg = reg + 1
mValue = reg

두 스 레 드 가 동시에 incr () 방법 을 실행 하면 업데이트 작업 중 하 나 를 잃 어 버 립 니 다.정확 한 실행 + 작업 을 확보 하기 위해 서 는 incr () 방법 을 'synchronized' 로 설명 해 야 합 니 다.이렇게 수정 한 후에 야 이 코드 는 단일 핵 다 중 스 레 드 환경 에서 정확하게 실 행 될 수 있다.
하지만 SMP 시스템 에 서 는 실 패 했 습 니 다.서로 다른 스 레 드 는 get () 방법 을 통 해 얻 은 값 이 다 를 수 있 습 니 다.우 리 는 일반적인 로드 방식 으로 이 값 을 읽 기 때문이다.우 리 는 get () 방법 을 synchronized 로 설명 함으로써 이 오 류 를 수정 할 수 있 습 니 다.이런 수정 을 통 해 이런 코드 가 정확 하 다.
불 행 히 도 발생 할 수 있 는 잠 금 경쟁 (lock contention) 을 소개 한 적 이 있 습 니 다. 프로그램의 성능 에 해 를 끼 칠 수 있 습 니 다.get () 방법 이 synchronized 라 는 것 을 제외 하고 mValue 를 "volatile" 이 라 고 설명 할 수 있 습 니 다.incr () 는 조금 느 려 지지 만 get () 방법 은 더욱 빨 라 집 니 다.따라서 읽 기 동작 이 쓰기 동작 보다 많 을 때 좋 은 방안 이 될 것 이다.(AtomicInteger 를 참고 하 십시오.)
다음은 다른 예시 입 니 다. 이전의 C 예시 와 약간 유사 합 니 다.
class MyGoodies {
    public int x, y;
}
class MyClass {
    static MyGoodies sGoodies;

    void initGoodies() {    // runs in thread 1
        MyGoodies goods = new MyGoodies();
        goods.x = 5;
        goods.y = 10;
        sGoodies = goods;
    }

    void useGoodies() {    // runs in thread 2
        if (sGoodies != null) {
            int i = sGoodies.x;    // could be 5 or 0
            ....
        }
    }
}

이 코드 에 도 문제 가 존재 합 니 다. sGoodies = goods 의 할당 작업 은 goods 구성원 변수 할당 전에 감지 될 수 있 습 니 다.volatile 성명 sGoodies 변 수 를 사용 하면 load 작업 이 atomic 라 고 생각 할 수 있 습 니 다.acquire_load (), 그리고 store 작업 을 atomic 로 생각 합 니 다.release_store()。
(sGoodies 의 인용 자체 가 volatile 일 뿐 내부 필드 에 접근 하 는 것 은 그렇지 않 습 니 다. 할당 문 z = sGoodies. x 는 volatile load MyClass. sGoodies 의 동작 을 수행 합 니 다. 그 다음 non - volatile 의 load 작업 이 수 반 됩 니 다.: sGoodies. x. 로 컬 참조 MyGoodies localGoods = sGoodies, z = localGoods. x 를 설정 하면 로 컬 참조 가 되 지 않 습 니 다.모든 volatile loads 를 실행 합 니 다.)
자바 프로그램 에서 더 많이 사용 되 는 또 다른 패 러 다 임 은 악명 높 은 'double - checked locking' 이다.
class MyClass {
    private Helper helper = null;

    public Helper getHelper() {
        if (helper == null) {
            synchronized (this) {
                if (helper == null) {
                    helper = new Helper();
                }
            }
        }
        return helper;
    }
}

위의 글 씨 는 MyClass 의 단일 예 를 얻 기 위해 서 입 니 다.getHelper () 를 통 해 이 인 스 턴 스 를 한 번 만 들 수 있 습 니 다.두 스 레 드 가 동시에 이 인 스 턴 스 를 만 드 는 것 을 피하 기 위해 서 입 니 다.만 든 동작 에 synchronize 메커니즘 을 추가 해 야 합 니 다.그러나, 우 리 는 이 코드 를 실행 할 때마다 'synchronized' 를 위해 추가 대 가 를 치 르 고 싶 지 않 습 니 다. 따라서 우 리 는 helper 대상 이 비어 있 을 때 만 자 물 쇠 를 추가 합 니 다.
단일 핵 시스템 에서 이것 은 정상적으로 일 할 수 없다.JIT 컴 파일 러 가 이 일 을 망 칠 수 있다.4) Appendix 의 "Double Checked Locking is Broken" Declaration 을 보고 더 많은 정 보 를 얻 거나 Josh Bloch 's Effective Java 책의 Item 71 ("Use lazy initialization judiciously") 을 보십시오.
SMP 시스템 에서 이 코드 를 실행 하면 추가 방식 을 도입 하면 실패 할 수 있 습 니 다.위의 코드 를 C 로 바 꾸 는 언어 는 다음 과 같다.
if (helper == null) {
    // acquire monitor using spinlock
    while (atomic_acquire_cas(&this.lock, 0, 1) != success)
        ;
    if (helper == null) {
        newHelper = malloc(sizeof(Helper));
        newHelper->x = 5;
        newHelper->y = 10;
        helper = newHelper;
    }
    atomic_release_store(&this.lock, 0);
}

이 문 제 는 더욱 뚜렷 해 졌 다. helper 의 store 작업 은 memory barrier 전에 발생 했 는데 이것 은 다른 스 레 드 가 store x / y 전에 비 어 있 는 값 을 관찰 할 수 있다 는 것 을 의미한다.
store helper 가 atomic 에서 실행 되 는 지 확인 해 봐 야 합 니 다.release_store () 방법 이후.다시 정렬 코드 를 통 해 자 물 쇠 를 추가 하지만 이것 은 유효 하지 않 습 니 다. 위로 이동 하 는 코드 때문에 컴 파일 러 는 원래 의 위치 로 이동 할 수 있 습 니 다: atomicrelease_store () 앞.(여 기 는 못 읽 었 으 니 다음 에 다시 읽 겠 습 니 다)
이 문 제 를 해결 할 수 있 는 두 가지 방법 이 있다.
외부 검 사 를 삭제 합 니 다.이것 은 우리 가 synchronized 코드 세그먼트 밖에서 어떠한 검사 도 하지 않 을 것 을 확보 했다
4. 567917. helper 는 volatile 이 라 고 성명 합 니 다.이러한 작은 수정 만으로 도 앞의 예제 에서 코드 는 자바 1.5 와 그 이후 버 전에 서 정상적으로 작 동 할 수 있 습 니 다
다음 예제 에 서 는 volatile 을 사용 하 는 2 가지 중요 한 문 제 를 보 여 줍 니 다.
class MyClass {
    int data1, data2;
    volatile int vol1, vol2;

    void setValues() {    // runs in thread 1
        data1 = 1;
        vol1 = 2;
        data2 = 3;
    }

    void useValues1() {    // runs in thread 2
        if (vol1 == 2) {
            int l1 = data1;    // okay
            int l2 = data2;    // wrong
        }
    }
    void useValues2() {    // runs in thread 2
        int dummy = vol2;
        int l1 = data1;    // wrong
        int l2 = data2;    // wrong
    }

useValues 1 () 을 주의 하 십시오. thread 2 가 vol 1 의 업데이트 작업 을 감지 하지 못 하면 데이터 1 이나 데이터 2 가 설 정 된 작업 을 알 수 없습니다.vol 1 의 업데이트 작업 을 관찰 하면 데이터 1 의 업데이트 작업 도 알 수 있 습 니 다.그러나 데이터 2 에 대해 서 는 추측 할 수 없습니다. store 작업 은 volatile store 이후 에 발생 했 기 때 문 입 니 다.
useValues 2 () 는 두 번 째 volatile 필드 를 사 용 했 습 니 다. vol 2 는 VM 에 memory barrier 를 생 성 하도록 강제 합 니 다.이것 은 통상 발생 하지 않 는 다.적절 한 'happens - before' 관 계 를 만 들 기 위해 서 는 두 스 레 드 모두 같은 volatile 필드 를 사용 해 야 합 니 다.thread 1 에서 vol 2 가 data 1 / data 2 이후 에 설정 되 었 다 는 것 을 알 아야 합 니 다.(The fact that this doesn’t work is probably obvious from looking at the code; the caution here is against trying to cleverly “cause” a memory barrier instead of creating an ordered series of accesses.)

좋은 웹페이지 즐겨찾기