Java 메모리 모델 소개

JMM(Java Memory Model)은 Java 런타임 환경에서 스레드의 동작을 설명합니다. 메모리 모델은 Java 언어의 의미 체계의 일부이며 특정 Java 시스템이 아닌 Java 전체를 위한 소프트웨어를 개발할 때 프로그래머가 기대할 수 있는 것과 기대해서는 안 되는 것을 설명합니다.

1995년에 개발된 원래 Java 메모리 모델(특히 "percolocal 메모리"를 나타냄)은 실패로 간주됩니다. 많은 최적화가 코드 안전성 보장을 잃지 않고는 이루어질 수 없습니다. 특히 다중 스레드 "단일"을 작성하는 몇 가지 옵션이 있습니다.

  • 싱글톤에 액세스하는 모든 행위(객체가 오래 전에 생성되어 아무 것도 변경되지 않는 경우에도)는 스레드 간 잠금을 유발합니다.
  • 또는 특정 상황에서 시스템은 미완성 외톨이를 발행합니다.
  • 또는 특정 상황에서 시스템은 두 개의 외톨이를 생성합니다.
  • 또는 디자인은 특정 기계의 동작에 따라 달라집니다.

따라서 메모리 메커니즘이 재설계되었습니다. 2005년 Java 5가 출시되면서 새로운 접근 방식이 제시되었으며 Java 14가 출시되면서 더욱 개선되었습니다.

새 모델은 세 가지 규칙을 기반으로 합니다.

규칙 #1 : 단일 스레드 프로그램은 유사 순차적으로 실행됩니다. 즉, 실제로 프로세서는 순서를 변경하면서 동시에 클럭당 여러 작업을 수행할 수 있지만 모든 데이터 종속성은 그대로 유지되므로 동작이 순차와 다르지 않습니다.

규칙 번호 2 : 뜬금없는 값은 없습니다. 변수를 읽으면(이 규칙이 적용되지 않을 수 있는 비휘발성 long 및 double 제외) 기본값(0) 또는 다른 명령에 의해 작성된 값이 반환됩니다.

그리고 규칙 번호 3 : 나머지 이벤트는 엄격한 부분 순서 관계로 연결된 경우 "이전에 실행"( 이전에 발생 ) 순서대로 실행됩니다.

이전에 발생

Leslie Lamport는 이전에 Happens 라는 개념을 제시했습니다 . 이것은 원자 명령(++ 및 -- 원자가 아님) 간에 도입된 엄격한 부분 순서 관계이며 "물리적으로 이전"을 의미하지 않습니다.

두 번째 팀은 첫 번째 팀이 변경한 사항을 "알게" 됩니다.

이전에 발생

예를 들어 다음과 같은 작업을 위해 하나가 다른 것보다 먼저 실행됩니다.

동기화 및 모니터:

  • 모니터 캡처( 잠금 방법 , 동기화된 시작 ) 및 그 이후에 동일한 스레드에서 발생하는 모든 일.
  • 모니터 반환(메소드 잠금 해제 , 동기화 종료) 및 그 이전에 동일한 스레드에서 발생하는 모든 것.
  • 모니터를 반환한 다음 다른 스레드에서 캡처합니다.

쓰기 및 읽기:

  • 임의의 변수에 쓴 다음 동일한 스트림에서 읽습니다.
  • 휘발성 변수에 쓰기 전에 동일한 스레드의 모든 것과 쓰기 자체. 휘발성 읽기 및 그 이후에 동일한 스레드의 모든 것.
  • 휘발성 변수에 쓴 다음 다시 읽습니다. 휘발성 쓰기는 모니터 반환과 같은 방식으로 메모리와 상호 작용하는 반면 읽기는 캡처와 같습니다. 한 스레드가 휘발성 변수에 썼고 두 번째 스레드가 그것을 찾았다면 쓰기 이전의 모든 것이 읽기 이후의 모든 것보다 먼저 실행됩니다. 그림을 참조하십시오.

개체 유지 관리:

  • 정적 초기화 및 개체 인스턴스에 대한 모든 작업.
  • 생성자의 최종 필드와 생성자 뒤의 모든 항목에 쓰기. 예외적으로 이전 발생 관계는 다른 규칙에 전이적으로 연결되지 않으므로 스레드 간 경합이 발생할 수 있습니다.
  • 객체 및 finalize() 에 대한 모든 작업 .

스트림 서비스:

  • 스레드 및 스레드의 모든 코드를 시작합니다.
  • 스레드 및 스레드의 모든 코드와 관련된 변수를 제로화합니다.
  • 스레드의 코드와 join() ; 스레드의 코드 및 isAlive() == false .
  • 스레드를 인터럽트() 하고 스레드가 중지되었음을 감지합니다.

작업 뉘앙스 전에 발생

이전 모니터 해제는 동일한 모니터를 획득하기 전에 발생합니다. 종료가 아니라 해제라는 점은 주목할 가치가 있습니다. 즉, 대기를 사용할 때 안전에 대해 걱정할 필요가 없습니다.

이 지식이 우리의 예를 수정하는 데 어떻게 도움이 되는지 봅시다. 이 경우 모든 것이 매우 간단합니다. 외부 확인을 제거하고 동기화를 그대로 둡니다. 이제 두 번째 스레드는 다른 스레드가 모니터를 해제한 후에만 모니터를 가져오기 때문에 모든 변경 사항을 볼 수 있습니다. 그리고 그는 모든 것이 초기화될 때까지 릴리스하지 않기 때문에 모든 변경 사항을 개별적으로가 아니라 한 번에 볼 수 있습니다.

public class Keeper {
    private Data data = null;

    public Data getData() {
        synchronized(this) {
            if(data == null) {
                data = new Data();
            }
        }

        return data;
    }
}

휘발성 변수에 쓰기는 동일한 변수에서 읽기 전에 발생합니다. 물론 우리가 만든 변경 사항은 버그를 수정하지만 원래 코드를 작성한 사람은 원래 코드를 다시 작성하여 매번 차단합니다. 휘발성 키워드는 저장할 수 있습니다. 실제로 문제의 진술은 휘발성으로 선언된 모든 항목을 읽을 때 항상 실제 값을 얻는다는 것을 의미합니다.

또한 앞서 말했듯이 휘발성 필드의 경우 쓰기는 항상(long 및 double 포함) 원자적 작업입니다. 또 다른 중요한 점: 다른 엔터티(예: 배열, 목록 또는 기타 클래스)에 대한 참조가 있는 휘발성 엔터티가 있는 경우 엔터티 자체에 대한 참조만 항상 "신선"하지만 모든 항목에 대한 참조는 아닙니다. 그것은 들어오는.

다시 이중 잠금 숫양으로 돌아갑니다. 휘발성을 사용하면 다음과 같은 상황을 해결할 수 있습니다.

public class Keeper {
    private volatile Data data = null;

    public Data getData() {
        if(data == null) {
            synchronized(this) {
                if(data == null) {
                    data = new Data();
                }
            }
        }
        return data;
    }
}

여기에 여전히 잠금이 있지만 데이터 == null인 경우에만 가능합니다. 휘발성 읽기를 사용하여 나머지 경우를 필터링합니다. 정확성은 휘발성 저장이 휘발성 읽기 이전에 발생하고 생성자에서 발생하는 모든 작업이 필드 값을 읽는 모든 사람에게 표시된다는 사실에 의해 보장됩니다.