자바 의 volatile 깊이 분석

7915 단어 자바validate
메모리 가시 성
volatile 은 자바 가 제공 하 는 경량급 동기 화 체제 로 병행 프로 그래 밍 에서 도 비교적 중요 한 역할 을 한다.synchronized 에 비해(synchronized 는 보통 중량급 자물쇠 라 고 함)volatile 은 경량급 입 니 다.synchronized 를 사용 할 때 발생 하 는 스 레 드 컨 텍스트 전환 에 따 른 엄 청 난 비용 보다 volatile 을 적당 하고 합 리 적 으로 사용 할 수 있다 면 좋 은 일이 될 것 입 니 다.
volatile 을 비교적 명확 하고 철저하게 이해 하기 위해 우 리 는 한 걸음 한 걸음 분석 했다.우선 다음 코드 를 살 펴 보 겠 습 니 다.

public class TestVolatile {
 boolean status = false;
 /**
  *      true
  */
 public void changeStatus(){
  status = true;
 }
 /**
  *     true, running。
  */
 public void run(){
  if(status){
   System.out.println("running....");
  }
 }
}
위의 이 예 는 다 중 스 레 드 환경 에서 스 레 드 A 가 change Status()방법 을 실행 한 후에 스 레 드 B 가 run()방법 을 실행 하면 출력'running...'을 보장 할 수 있 습 니까?
정 답 은 NO!
이 결론 은 좀 의 심 스 럽 고 이해 할 수 있다.단일 스 레 드 모델 에서 change Status 방법 을 실행 하고 run 방법 을 실행 하면'running...'을 정확하게 출력 할 수 있 기 때 문 입 니 다.그러나 다 중 스 레 드 모델 에 서 는 이런 보증 을 할 수 없다.공유 변수 status 에 있어 스 레 드 A 의 수정 은 스 레 드 B 에 있어'보이 지 않 음'이기 때문이다.스 레 드 B 는 이때 status 가 true 로 수 정 된 것 을 관측 할 수 없 을 수도 있다 는 것 이다.그렇다면 가시 성 은 무엇 일 까?
가시 성 이란 한 라인 이 공유 변수의 값 을 수정 하면 새로운 값 은 다른 라인 에 있어 서 즉시 알 수 있 는 것 을 말한다.상술 한 예 에서 메모리 의 가시 성 을 실현 할 방법 이 없 음 이 분명 하 다.
자바 메모리 모델
왜 이런 상황 이 발생 했 는 지 JMM(자바 메모리 모델)부터 알 아 봐 야 합 니 다.
자바 가상 머 신 은 자체 메모리 모델(자바 메모리 모델,JMM)이 있 습 니 다.JMM 은 각종 하드웨어 와 운영 체제 의 메모리 접근 차 이 를 차단 하여 자바 프로그램 이 각종 플랫폼 에서 일치 하 는 메모리 접근 효 과 를 얻 을 수 있 도록 합 니 다.
JMM 은 공유 변 수 를 언제 다른 스 레 드 에 기록 할 지 결정 합 니 다.JMM 은 스 레 드 와 메 인 메모리 간 의 추상 적 인 관 계 를 정의 합 니 다.공유 변 수 는 메 인 메모리(Main Memory)에 저장 되 고 모든 스 레 드 는 개인 로 컬 메모리(Local Memory)가 있 습 니 다.로 컬 메모 리 는 이 스 레 드 에 사 용 된 메 인 메모리 의 복사 본 을 저장 합 니 다.스 레 드 는 변수의 모든 작업 을 작업 메모리 에서 해 야 하 며,메 인 메모리 의 변 수 를 직접 읽 을 수 없습니다.이 세 사람 간 의 상호 관 계 는 다음 과 같다.

 
주의해 야 할 것 은 JMM 은 추상 적 인 메모리 모델 이기 때문에 로 컬 메모리 라 는 것 은 메 인 메모리 가 모두 추상 적 인 개념 이 므 로 반드시 cpu 캐 시 와 물리 적 메모리 에 진실 하 게 대응 하 는 것 은 아니다.물론 이해 의 목적 이 라면 이렇게 대응 해도 안 된다.
JMM 의 간단 한 정 의 를 알 게 된 후에 문 제 는 쉽게 이해 할 수 있 습 니 다.일반적인 공유 변수 에 있어 서 예 를 들 어 우리 가 상기 한 status,스 레 드 A 는 이 동작 을 true 로 수정 하여 온라인 프로 세 스 A 의 로 컬 메모리 에서 발생 했 습 니 다.이때 메 인 메모리 에 동기 화 되 지 않 았 습 니 다.스 레 드 B 는 status 의 초기 값 false 를 캐 시 했 습 니 다.이때 status 의 값 이 수정 되 었 는 지 관측 되 지 않 았 기 때문에 상기 문 제 를 일 으 켰 습 니 다.그렇다면 이러한 공유 변 수 는 다 중 스 레 드 모델 에서 보이 지 않 는 것 을 어떻게 해결 합 니까?거 친 방식 은 자 연 스 럽 게 자 물 쇠 를 채 우 는 것 이지 만 이곳 에 서 는 synchronized 나 Lock 을 사용 하 는 방식 이 너무 무 거 워 모 기 를 때 린 다 는 뜻 이다.비교적 합 리 적 인 방식 은 사실 volatile 입 니 다.
volatile 은 두 가지 특성 을 가지 고 있 는데 첫째,공유 변수 가 모든 라인 에 대한 가시 성 을 확보 하 는 것 이다.공유 변 수 를 volatile 로 설명 하면 다음 과 같은 효과 가 있 습 니 다.
1.volatile 변 수 를 쓸 때 JMM 은 이 스 레 드 에 대응 하 는 로 컬 메모리 의 변 수 를 주 메모리 로 강제로 새로 고침 합 니 다.
2.이 쓰 기 는 다른 스 레 드 의 캐 시 를 잘못 사용 할 수 있 습 니 다.
위의 예 는 status 를 volatile 로 설명 하면 스 레 드 A 가 트 루 로 수정 할 때 스 레 드 B 는 즉시 알 수 있 습 니 다.

 volatile boolean status = false;
복합 류 조작 에 유의 하 다.
그러나 주의해 야 할 것 은 우리 가 volatile 과 synchronized 를 비교 해 왔 다 는 것 이다.단지 이 두 키 워드 는 특정한 메모리 의미 에서 공통점 이 있 기 때문이다.volatile 은 synchronized 를 완전히 대체 할 수 없고 경량급 자물쇠 이기 때문에 많은 장면 에서 volatile 은 감당 할 수 없다.이 예 를 보 세 요.

package test;
import java.util.concurrent.CountDownLatch;
/**
 * Created by chengxiao on 2017/3/18.
 */
public class Counter {
 public static volatile int num = 0;
 //  CountDownLatch          
 static CountDownLatch countDownLatch = new CountDownLatch(30);
 public static void main(String []args) throws InterruptedException {
  //  30         
  for(int i=0;i<30;i++){
   new Thread(){
    public void run(){
     for(int j=0;j<10000;j++){
      num++;//    
     }
     countDownLatch.countDown();
    }
   }.start();
  }
  //         
  countDownLatch.await();
  System.out.println(num);
 }
}
실행 결과:
224291
이 예 에 대해 일부 학생 들 은 volatile 로 장 식 된 공유 변수 가 가시 성 을 확보 할 수 있다 면 결 과 는 300000 이 어야 하지 않 겠 는가?
문 제 는 num++라 는 조작 에 있 습 니 다.num+는 원자 적 인 조작 이 아니 라 복합 조작 이기 때 문 입 니 다.우 리 는 이 조작 이 해 를 이유 로 이 세 단계 로 구성 되 었 다 는 것 을 간단하게 말 할 수 있다.
1.읽 기
2.더하기 1
3.할당
따라서 다 중 스 레 드 환경 에서 스 레 드 A 가 num 을 로 컬 메모리 에 읽 을 수 있 습 니 다.이때 다른 스 레 드 는 num 을 많이 증가 시 켰 을 수도 있 습 니 다.스 레 드 A 는 만 료 된 num 을 자체 적 으로 추가 하고 메 인 저장 소 에 다시 썼 습 니 다.결국 num 의 결 과 는 기대 에 맞지 않 고 30000 보다 작 습 니 다.
num+작업 의 원자 문제 해결
num++와 같은 복합 류 의 조작 에 대해 자바 와 가방 에 있 는 원자 조작 류 원자 조작 류 는 CAS 를 순환 하 는 방식 으로 원자 성 을 확보 할 수 있 습 니 다.

/**
 * Created by chengxiao on 2017/3/18.
 */
public class Counter {
  //       
 public static AtomicInteger num = new AtomicInteger(0);
 //  CountDownLatch          
 static CountDownLatch countDownLatch = new CountDownLatch(30);
 public static void main(String []args) throws InterruptedException {
  //  30         
  for(int i=0;i<30;i++){
   new Thread(){
    public void run(){
     for(int j=0;j<10000;j++){
      num.incrementAndGet();//    num++,    CAS  
     }
     countDownLatch.countDown();
    }
   }.start();
  }
  //         
  countDownLatch.await();
  System.out.println(num);
 }
}
실행 결과
300000
원자 류 작업 의 기본 원 리 는 뒤의 장 에서 소개 할 것 이 며,여 기 는 더 이상 군말 하지 않 을 것 이다.
명령 정렬 금지
volatile 또 하나의 특성 이 있 습 니 다.명령 재 정렬 최적화 금지.
재 정렬 은 컴 파일 러 와 프로세서 가 프로그램의 성능 을 최적화 하기 위해 명령 서열 을 정렬 하 는 수단 을 말한다.그러나 재 정렬 도 일정한 규칙 을 지 켜 야 한다.
1.정렬 작업 은 데이터 의존 관계 가 존재 하 는 작업 에 대해 정렬 을 다시 하지 않 습 니 다.
예 를 들 어 a=1;b=a; 이 명령 서열 은 두 번 째 작업 이 첫 번 째 작업 에 의존 하기 때문에 컴 파일 할 때 와 프로세서 가 실 행 될 때 이 두 작업 은 다시 정렬 되 지 않 습 니 다.
2.정렬 을 다시 하 는 것 은 성능 을 최적화 하기 위 한 것 이지 만 아무리 정렬 을 다시 하 더 라 도 단일 라인 에서 프로그램의 실행 결 과 는 바 꿀 수 없습니다.
예 를 들 어 a=1;b=2;c=a+b 이 세 가지 조작,첫 번 째 단계(a=1)와 두 번 째 단계(b=2)는 데이터 의존 관계 가 존재 하지 않 기 때문에 정렬 을 다시 할 수 있 습 니 다.그러나 c=a+b 이 조작 은 정렬 을 다시 하지 않 습 니 다.최종 결 과 는 반드시 c=a+b=3 이 어야 하기 때 문 입 니 다.
다시 정렬 하 는 것 은 단일 스 레 드 모드 에서 반드시 최종 결과 의 정확성 을 확보 할 것 입 니 다.그러나 다 중 스 레 드 환경 에서 문제 가 발생 했 습 니 다.예 를 들 어 첫 번 째 TestVolatile 의 예 를 조금 개선 하고 공유 변 수 를 추가 합 니 다 a.

public class TestVolatile {
 int a = 1;
 boolean status = false;
 /**
  *      true
  */
 public void changeStatus(){
  a = 2;//1
  status = true;//2
 }
 /**
  *     true, running。
  */
 public void run(){
  if(status){//3
   int b = a+1;//4
   System.out.println(b);
  }
 }
}
스 레 드 A 가 change Status 를 실행 한 후에 스 레 드 B 가 run 을 실행 한다 고 가정 하면 우 리 는 4 곳 에서 b 가 반드시 3 이 라 고 보장 할 수 있 습 니까?
답 은 여전히 장담 할 수 없다!b 가 여전히 2 일 수도 있다.위 에서 언급 한 바 와 같이 프로그램의 병행 도 를 제공 하기 위해 컴 파 일 러 와 프로 세 서 는 명령 을 다시 정렬 할 수 있 으 며,상례 의 1 과 2 는 데이터 의존 관계 가 존재 하지 않 기 때문에 다시 정렬 될 수 있 으 며,status=true 를 먼저 실행 하고 a=2 를 실행 합 니 다.이때 스 레 드 B 는 4 곳 에 순조롭게 도착 할 것 이 고 스 레 드 A 중 a=2 는 아직 실행 되 지 않 았 기 때문에 b=a+1 의 결과 도 2 와 같 을 수 있다.
volatile 키 워드 를 사용 하여 공유 변 수 를 수식 하면 이러한 정렬 을 금지 할 수 있 습 니 다.volatile 로 공유 변 수 를 수식 하면 컴 파일 할 때 명령 시퀀스 에 메모리 장벽 을 삽입 하여 특정 유형의 프로세서 정렬 을 금지 합 니 다.
volatile 명령 의 정렬 을 금지 하 는 규칙 도 있 습 니 다.간단하게 열거 하 십시오.
1.두 번 째 작업 이 voaltile 로 작성 되 었 을 때 첫 번 째 작업 이 무엇 이 든 다시 정렬 할 수 없습니다.
2.현지의 한 조작 은 volatile 이 읽 을 때 두 번 째 조작 이 무엇 이 든 다시 정렬 할 수 없다.
3.첫 번 째 작업 이 volatile 로 작성 되 었 을 때 두 번 째 작업 은 volatile 로 읽 었 을 때 정렬 할 수 없습니다.
요약:
간단하게 정리 하면 volatile 은 경량급 동기 화 체제 로 주로 두 가지 특성 이 있다.하 나 는 공유 변수 가 모든 스 레 드 에 대한 가시 성 을 확보 하 는 것 이다.둘째,명령 재 정렬 최적화 금지.또한 주의해 야 할 것 은 volatile 은 하나의 공유 변수 에 대한 읽 기/쓰기 가 원자 성 을 가지 지만 num+와 같은 복합 작업 은 volatile 이 원자 성 을 보장 할 수 없다 는 것 이다.물론 글 에서 도 해결 방안 을 제시 했다.즉,가방 에 있 는 원자 조작 류 를 사용 하고 발송 하 며 고리 CAS 방식 으로 num+작업 의 원자 성 을 확보 하 는 것 이다.원자 조작 류 에 대해 서 는 후속 글 에서 소개 한다.여러분 저희 사이트 에 주목 해 주세요!

좋은 웹페이지 즐겨찾기