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인 경우에만 가능합니다. 휘발성 읽기를 사용하여 나머지 경우를 필터링합니다. 정확성은 휘발성 저장이 휘발성 읽기 이전에 발생하고 생성자에서 발생하는 모든 작업이 필드 값을 읽는 모든 사람에게 표시된다는 사실에 의해 보장됩니다.
GO TO FULL VERSION