동시 프로그래밍 모델 분류
동시 프로그래밍에서는 스레드 간 통신 방법과 스레드 간 동기화 방법이라는 두 가지 주요 문제를 처리해야 합니다(여기서 스레드는 동시에 실행되는 활성 엔터티를 나타냄). 통신은 스레드가 정보를 교환하는 메커니즘을 나타냅니다. 명령형 프로그래밍에는 스레드 간에 공유 메모리와 메시지 전달이라는 두 가지 통신 메커니즘이 있습니다.
공유 메모리 동시성 모델에서는 프로그램의 공통 상태가 스레드 간에 공유되고 스레드는 메모리에 공통 상태를 쓰고 읽어 암시적으로 통신합니다. 메시지 전달 동시성 모델에서는 스레드 간에 공개 상태가 없으며 스레드는 명시적으로 메시지를 보내 명시적으로 통신해야 합니다.
동기화란 서로 다른 스레드 간에 작업이 발생하는 상대적 순서를 제어하기 위해 프로그램에서 사용하는 메커니즘을 의미합니다. 공유 메모리 동시성 모델에서는 동기화가 명시적으로 수행됩니다. 프로그래머는 메서드나 코드 조각이 스레드 간에 독점적으로 실행되어야 함을 명시적으로 지정해야 합니다. 메시지 전달 동시성 모델에서는 메시지 전송이 메시지 수신보다 먼저 이루어져야 하므로 동기화가 암시적으로 수행됩니다.
Java의 동시성은 공유 메모리 모델을 채택합니다. Java 스레드 간의 통신은 항상 암시적으로 수행되며 전체 통신 프로세스는 프로그래머에게 완전히 투명합니다. 다중 스레드 프로그램을 작성하는 Java 프로그래머가 암시적인 스레드 간 통신이 어떻게 작동하는지 이해하지 못한다면 그는 온갖 종류의 이상한 메모리 가시성 문제에 직면할 가능성이 높습니다.
Java 메모리 모델의 추상화
Java에서는 모든 인스턴스 필드, 정적 필드 및 배열 요소가 힙 메모리에 저장되며 힙 메모리는 스레드 간에 공유됩니다(이 기사에서는 "공유 변수"를 사용합니다). " 이 용어는 인스턴스 필드, 정적 필드 및 배열 요소를 나타냅니다. 지역 변수, 메서드 정의 매개변수(Java 언어 사양에서는 형식적 메서드 매개변수라고 함) 및 예외 처리기 매개변수는 스레드 간에 공유되지 않으며 메모리 가시성 문제가 없으며 메모리 모델의 영향을 받지 않습니다.
Java 스레드 간의 통신은 Java 메모리 모델(이 문서에서는 JMM이라고 함)에 의해 제어됩니다. 이 모델은 한 스레드의 공유 변수 쓰기가 다른 스레드에 표시되는 시점을 결정합니다. 추상적인 관점에서 JMM은 스레드와 주 메모리 간의 추상 관계를 정의합니다. 스레드 간의 공유 변수는 주 메모리(주 메모리)에 저장되며 각 스레드는 공유 메모리의 복사본인 개인 로컬 메모리(로컬 메모리)를 갖습니다. 스레드가 읽고 쓰는 변수는 로컬 메모리에 저장됩니다. 로컬 메모리는 JMM의 추상적인 개념이며 실제로 존재하지 않습니다. 캐시, 쓰기 버퍼, 레지스터, 기타 하드웨어 및 컴파일러 최적화를 다룹니다. Java 메모리 모델의 추상적인 개략도는 다음과 같습니다.
위 그림에서 스레드 A와 스레드 B가 통신을 하려면 다음 두 가지 과정을 거쳐야 합니다. 단계:
먼저 스레드 A는 로컬 메모리 A의 업데이트된 공유 변수를 주 메모리로 새로 고칩니다.
그런 다음 스레드 B는 스레드 A가 이전에 업데이트한 공유 변수를 읽기 위해 메인 메모리로 이동합니다.
다음은 이 두 단계를 설명하는 개략도입니다.
위 그림에 표시된 것처럼 로컬 메모리 A와 B에는 공유 메모리 복사본이 있습니다. 주 메모리의 변수 x. 처음에는 이 세 메모리의 x 값이 모두 0이라고 가정합니다. 스레드 A가 실행 중일 때 업데이트된 x 값(값이 1이라고 가정)을 자체 로컬 메모리 A에 임시 저장합니다. 스레드 A와 스레드 B가 통신해야 할 때 스레드 A는 먼저 로컬 메모리의 수정된 x 값을 주 메모리로 새로 고칩니다. 이때 주 메모리의 x 값은 1이 됩니다. 이후 스레드 B는 스레드 A의 업데이트된 x 값을 읽기 위해 메인 메모리로 이동합니다. 이때 스레드 B의 로컬 메모리의 x 값도 1이 됩니다.
전체적으로 보면 이 두 단계는 본질적으로 스레드 A가 스레드 B에게 메시지를 보내는 것이며, 이 통신 과정은 메인 메모리를 거쳐야 합니다. JMM은 기본 메모리와 각 스레드의 로컬 메모리 간의 상호 작용을 제어하여 Java 프로그래머에게 메모리 가시성을 보장합니다.
재순서
프로그램 실행 시 성능을 향상시키기 위해 컴파일러와 프로세서는 명령어를 재정렬하는 경우가 많습니다. 재정렬에는 세 가지 유형이 있습니다.
컴파일러에 최적화된 재정렬. 컴파일러는 단일 스레드 프로그램의 의미를 변경하지 않고도 문의 실행 순서를 다시 정렬할 수 있습니다.
명령어 수준 병렬 재정렬. 최신 프로세서는 ILP(명령 수준 병렬 처리)를 사용하여 여러 명령을 겹치는 방식으로 실행합니다. 데이터 종속성이 없으면 프로세서는 명령문이 기계 명령에 해당하는 순서를 변경할 수 있습니다.
메모리 시스템 재정렬. 프로세서는 캐시와 읽기/쓰기 버퍼를 사용하기 때문에 로드 및 저장 작업이 순서 없이 실행되는 것처럼 보일 수 있습니다.
Java 소스 코드에서 실제로 실행된 최종 명령 시퀀스까지 다음 세 가지 재정렬을 거칩니다.
위 1은 컴파일러 재정렬에 속하고, 2와 3은 프로세서 재정렬에 속합니다. 이러한 재정렬로 인해 다중 스레드 프로그램에서 메모리 가시성 문제가 발생할 수 있습니다. 컴파일러의 경우 JMM의 컴파일러 재정렬 규칙은 특정 유형의 컴파일러 재정렬을 금지합니다(모든 컴파일러 재정렬이 금지되는 것은 아닙니다). 프로세서 재정렬의 경우 JMM의 프로세서 재정렬 규칙에서는 명령 시퀀스를 생성할 때 Java 컴파일러가 특정 유형의 메모리 장벽(인텔에서는 이를 메모리 펜스라고 함) 명령을 삽입하고 메모리 장벽 명령을 사용하여 특정 유형의 프로세서 재정렬을 금지하도록 요구합니다(모든 프로세서가 아님). 재정렬을 비활성화해야 합니다). JMM은 다양한 컴파일러 및 프로세서 플랫폼에서 특정 유형의 컴파일러 재정렬 및 프로세서 재정렬을 금지하여 프로그래머에게 일관된 메모리를 보장하는 언어 수준 메모리 모델입니다. 프로세서 재정렬 및 메모리 배리어 지침최신 프로세서는 쓰기 버퍼를 사용하여 메모리에 기록된 데이터를 임시로 저장합니다. 쓰기 버퍼는 명령 파이프라인을 계속 실행하고 데이터가 메모리에 기록될 때까지 기다리는 동안 프로세서가 정지하여 발생하는 지연을 방지합니다. 동시에 일괄 처리로 쓰기 버퍼를 새로 고치고 쓰기 버퍼의 동일한 메모리 주소에 대한 여러 쓰기를 병합함으로써 메모리 버스 사용량을 줄일 수 있습니다. 쓰기 버퍼에는 많은 이점이 있지만 각 프로세서의 쓰기 버퍼는 해당 프로세서에만 표시됩니다. 이 기능은 메모리 작업의 실행 순서에 중요한 영향을 미칩니다. 프로세서가 메모리 작업을 읽고 쓰는 순서는 메모리가 실제로 작업을 읽고 쓰는 순서와 반드시 일치하지는 않습니다. 구체적인 설명은 다음 예를 참조하세요. 프로세서 A프로세서 Ba = 1; //A1x = b //A2b = 2 ; / /B1y = a; //B2초기 상태: a = b = 0프로세서는 결과를 얻기 위해 실행을 허용합니다: x = y = 0 처리 프로세서 A와 프로세서 B가 프로그램 순서에 따라 병렬로 메모리 액세스를 수행한다고 가정하지만 결국 결과는 x = y = 0이 될 수 있습니다. 구체적인 이유는 아래 그림과 같습니다. 여기서 프로세서 A와 프로세서 B는 공유 변수를 자신의 쓰기 버퍼(A1, B1)에 동시에 쓴 다음 메모리에서 다른 공유 변수(A2, B2)를 읽고 마지막으로 스스로 쓸 수 있습니다. 더티 데이터 캐시 영역에 저장된 내용은 메모리(A3, B3)로 플러시됩니다. 이 타이밍에 실행하면 프로그램은 x = y = 0이라는 결과를 얻을 수 있습니다. 실제 메모리 작업 순서로 판단하면 쓰기 작업 A1은 프로세서 A가 A3을 실행하여 자체 쓰기 캐시를 새로 고칠 때까지 실제로 실행되지 않습니다. 프로세서 A는 A1->A2 순서로 메모리 작업을 수행하지만 실제로 메모리 작업이 발생하는 순서는 A2->A1입니다. 이때, 프로세서 A의 메모리 동작 순서가 재정렬된다(프로세서 B의 상황은 프로세서 A의 상황과 동일하므로 여기서는 자세히 설명하지 않겠다). 여기서 핵심은 쓰기 버퍼가 자체 프로세서에만 표시되므로 프로세서가 메모리 작업을 수행하는 순서가 메모리 작업이 수행되는 실제 순서와 일치하지 않게 된다는 것입니다. 최신 프로세서는 쓰기 버퍼를 사용하기 때문에 최신 프로세서에서는 쓰기-읽기 작업의 순서를 변경할 수 있습니다. 다음은 일반 프로세서에서 허용되는 재정렬 유형 목록입니다. Load-LoadLoad-StoreStore-StoreStore-Load데이터 종속성sparc-TSONNNYNx86NNNYNia64YYYYNPowerPCYYYYN켜기 테이블 셀의 "N"은 프로세서가 두 작업의 재정렬을 허용하지 않음을 나타내고 "Y"는 재정렬이 허용됨을 나타냅니다. 위 표에서 일반 프로세서는 Store-Load 재정렬을 허용하지만, 일반 프로세서는 데이터 종속성이 있는 작업 재정렬을 허용하지 않는다는 것을 알 수 있습니다. sparc-TSO 및 x86은 쓰기-읽기 작업의 재정렬만 허용하는 상대적으로 강력한 프로세서 메모리 모델을 가지고 있습니다(둘 다 쓰기 버퍼를 사용하기 때문입니다). ※참고 1: sparc-TSO는 TSO(Total Store Order) 메모리 모델에서 실행될 때 sparc 프로세서의 특성을 나타냅니다. ※참고 2: 위 표의 x86에는 x64 및 AMD64가 포함됩니다. ※참고 3: ARM 프로세서의 메모리 모델은 PowerPC 프로세서의 메모리 모델과 매우 유사하므로 이 기사에서는 이를 무시합니다. ※참고 4: 데이터 종속성에 대해서는 나중에 구체적으로 설명합니다. 메모리 가시성을 보장하기 위해 Java 컴파일러는 생성된 명령어 시퀀스의 적절한 위치에 메모리 장벽 명령어를 삽입하여 특정 유형의 프로세서 재정렬을 금지합니다. JMM은 메모리 배리어 명령을 다음 네 가지 범주로 나눕니다. 배리어 유형명령 예설명LoadLoad BarriersLoad1 Load2Load2 전에 Load1 데이터 로드를 보장합니다. 모든 후속 로드 명령어의 로드. StoreStore BarriersStore1; StoreStore; Store2 Store2에 저장하고 모든 후속 저장 명령을 수행하기 전에 Store1 데이터가 다른 프로세서에 표시되도록 합니다(메모리에 플러시됨). LoadStore BarriersLoad1; LoadStore; Store2 Store2 이전에 Load1 데이터가 로드되고 모든 후속 저장 명령이 메모리에 플러시되도록 합니다. StoreLoad BarriersStore1; StoreLoad2 Store1 데이터가 Load2 및 모든 후속 로드 명령에 의해 로드되기 전에 다른 프로세서에 표시되도록 합니다. StoreLoad Barriers는 Barrier 뒤의 메모리 액세스 명령어를 실행하기 전에 Barrier 앞의 모든 메모리 액세스 명령어(저장 및 로드 명령어)가 완료되도록 합니다. StoreLoad Barriers는 동시에 다른 세 가지 장벽의 효과를 갖는 "다목적" 장벽입니다. 대부분의 최신 다중 프로세서는 이 장벽을 지원합니다(다른 유형의 장벽은 모든 프로세서에서 지원되지 않을 수 있음). 현재 프로세서는 일반적으로 쓰기 버퍼의 모든 데이터를 메모리로 플러시(버퍼 완전 플러시)해야 하기 때문에 이 장벽을 실행하는 데 비용이 많이 들 수 있습니다. 발생 전JDK5부터 Java는 새로운 JSR-133 메모리 모델을 사용합니다(달리 명시하지 않는 한 이 기사에서는 JSR-133 메모리 모델에 중점을 둡니다). JSR-133은 작업 간의 메모리 가시성을 설명하는 사전 발생 개념을 제안합니다. 한 작업의 결과를 다른 작업에서 볼 수 있어야 하는 경우 두 작업 간에 사전 발생 관계가 있어야 합니다. 여기에 언급된 두 가지 작업은 하나의 스레드 내에서 또는 서로 다른 스레드 간에 수행될 수 있습니다. 프로그래머와 밀접하게 관련된 사전 발생 규칙은 다음과 같습니다. 프로그램 순서 규칙: 스레드의 각 작업은 해당 스레드의 후속 작업보다 먼저 발생합니다. 모니터 잠금 규칙: 모니터 잠금 해제는 모니터 잠금이 잠기기 전에 발생합니다. 휘발성 변수 규칙: 휘발성 필드에 쓰기는 이 휘발성 필드를 읽기 전에 발생합니다. 전이성: A가 B보다 먼저 발생하고 B가 C보다 먼저 발생하면 A가 C보다 먼저 발생합니다. 두 작업 사이에는 사전 발생 관계가 있다는 점에 유의하세요. 이는 전자 작업이 후자 작업보다 먼저 실행되어야 한다는 의미는 아닙니다! 이전에 발생하는 작업은 이전 작업(실행 결과)이 다음 작업에 표시되고, 첫 번째 작업이 두 번째 작업에 표시되고 순서가 지정되는 것만 요구합니다. 이전 발생의 정의는 매우 미묘합니다. 다음 기사에서는 이전 발생이 이러한 방식으로 정의된 이유를 구체적으로 설명합니다. thought-before와 JMM의 관계는 아래 그림과 같습니다. 위 그림에 표시된 것처럼 사전 발생 규칙은 일반적으로 여러 컴파일러 재정렬 규칙 및 프로세서 재정렬 규칙에 해당합니다. Java 프로그래머의 경우 사전 발생 규칙은 간단하고 이해하기 쉽습니다. 이를 통해 프로그래머는 JMM에서 제공하는 메모리 가시성 보장을 이해하기 위해 복잡한 재정렬 규칙과 이러한 규칙의 특정 구현을 배울 수 없습니다. 위 내용은 자바 메모리 모델의 기본 부분에 대한 심층 분석입니다. 더 많은 관련 내용은 PHP 중국어 홈페이지(m.sbmmt.com)를 참고해주세요!