이 Java 메모리 모델은 Java 가상 머신이 컴퓨터 메모리(RAM)와 작동하는 방식을 지정합니다. 이 Java 가상 머신은 전체 컴퓨터의 모델이므로 이 모델에는 자연스럽게 Java 메모리 모델이라고도 하는 메모리 모델이 포함됩니다.
동시 프로그램을 올바르게 설계하려면 Java 메모리 모델을 이해하는 것이 중요합니다. 이 Java 메모리 모델은 서로 다른 스레드가 다른 스레드가 작성한 공유 변수의 값을 언제 어떻게 볼 수 있는지, 공유 변수에 동기적으로 액세스하는 방법을 나타냅니다.
원래 Java 메모리 모델이 부족해서 Java 1.5에서 Java 메모리 모델이 개선되었습니다. 이 버전의 Java 메모리 모델은 Java 8에서 계속 사용됩니다.
내부 Java 메모리 모델
Java 메모리 모델은 JVM 내부에서 스레드 스택과 힙으로 나누어 사용됩니다. 이 다이어그램은 논리적 관점에서 메모리 모델을 보여줍니다.
Java Virtual Machine에서 실행되는 각 스레드에는 자체 스레드 스택이 있습니다. 스레드 스택에는 이 스레드가 현재 실행 지점까지 호출한 메서드에 대한 정보가 포함되어 있습니다. 우리는 이것을 "호출 스택"이라고 부르기도 합니다. 스레드가 코드를 실행하면 이 호출 스택이 변경됩니다.
이 스레드 스택에는 실행 중인 각 메서드(호출 스택의 모든 메서드)에 대한 모든 지역 변수도 포함됩니다. 스레드는 자신의 스레드 스택에만 액세스할 수 있습니다. 한 스레드에서 생성된 지역 변수는 다른 모든 스레드에서 볼 수 없습니다. 두 스레드가 완전히 동일한 코드를 실행하더라도 두 스레드는 여전히 자체 지역 변수를 생성합니다. 따라서 각 스레드에는 자체 버전의 지역 변수가 있습니다.
기본 유형(boolean, byte, short, char, int, long, float, double)의 모든 지역 변수는 스레드 스택에 완전히 저장되므로 다른 스레드에서 볼 수 없습니다. 스레드는 기본 유형 변수의 복사본을 다른 스레드에 전달할 수 있지만 여전히 기본 유형의 지역 변수를 공유할 수는 없습니다.
이 힙에는 객체를 생성한 스레드에 관계없이 애플리케이션에서 생성된 모든 객체가 포함됩니다. 여기에는 기본 유형(예: Byte, Integer, Long 등)의 객체 버전이 포함됩니다. 객체가 생성되어 지역 변수에 할당되거나 다른 객체의 멤버 변수가 생성되는지 여부에 관계없이 객체는 여전히 힙에 저장됩니다.
다음은 이 호출 스택과 스레드 스택에 저장된 지역 변수, 힙에 저장된 객체를 보여주는 다이어그램입니다.
a 지역 변수는 기본 유형일 수 있으며, 이 경우 스레드 스택에 완전히 저장됩니다.
지역 변수는 객체 참조일 수 있습니다. 이 시나리오에서 참조(지역 변수)는 스레드 스택에 저장되지만 개체 자체는 힙에 저장됩니다.
객체에는 메소드가 포함될 수 있으며 이러한 메소드에는 지역 변수가 포함됩니다. 이 메소드가 속한 객체가 힙에 저장되어 있더라도 이러한 지역 변수는 스레드 스택에도 저장됩니다.
객체의 멤버 변수는 객체 자체와 함께 힙에 저장됩니다. 이 멤버 변수가 기본 유형인 경우뿐만 아니라 개체에 대한 참조인 경우에도 마찬가지입니다.
정적 클래스 변수도 힙에 저장됩니다.
힙에 있는 개체는 이 개체에 대한 참조가 있는 모든 스레드에서 액세스할 수 있습니다. 스레드가 개체에 액세스할 때 개체의 멤버 변수에도 액세스할 수 있습니다. 두 스레드가 동시에 동일한 개체에 대한 메서드를 호출하면 개체의 멤버 변수에 동시에 액세스하지만 각 스레드는 자체 지역 변수 복사본을 갖게 됩니다.
위의 설명을 바탕으로 한 그림은 다음과 같습니다.
두 개의 스레드에는 일련의 지역 변수가 있습니다. 지역 변수 중 하나(로캘 변수 2)는 힙의 공통 개체(개체 3)를 가리킵니다. 두 스레드는 각각 동일한 개체에 대해 서로 다른 참조를 갖습니다. 그들이 참조하는 지역 변수는 스레드 스택에 저장되지만 이 두 가지 다른 참조가 가리키는 동일한 개체는 힙에 있습니다.
이 공유 객체(객체 3)가 어떻게 객체 2와 객체 4를 멤버 변수로 참조하는지 주목하세요(그림에서 화살표로 표시됨). Object3의 이러한 변수 참조를 통해 두 스레드 모두 Object2 및 Object4에 액세스할 수도 있습니다.
이 다이어그램은 힙에 있는 서로 다른 두 개체를 가리키는 로컬 변수도 보여줍니다. 이 시나리오에서 이 참조는 동일한 개체가 아닌 두 개의 서로 다른 개체(개체 1과 개체 5)를 가리킵니다. 이론적으로 두 스레드가 모두 이 두 개체에 대한 참조를 갖고 있는 경우 두 개체는 개체 1과 개체 5에 모두 액세스할 수 있습니다. 그러나 다이어그램에서 각 스레드는 이 두 개체에 대한 참조만 갖습니다.
그럼 위 그림의 메모리 구조는 어떤 코드가 될까요? 음, 다음 코드와 같은 짧은 대답은 다음과 같습니다.
public class MyRunnable implements Runnable() { public void run() { methodOne(); } public void methodOne() { int localVariable1 = 45; MySharedObject localVariable2 = MySharedObject.sharedInstance; //... do more with local variables. methodTwo(); } public void methodTwo() { Integer localVariable1 = new Integer(99); //... do more with local variable. } }
public class MySharedObject { //static variable pointing to instance of MySharedObject public static final MySharedObject sharedInstance = new MySharedObject(); //member variables pointing to two objects on the heap public Integer object2 = new Integer(22); public Integer object4 = new Integer(44); public long member1 = 12345; public long member1 = 67890; }
두 개의 스레드가 이 실행 메서드를 실행하는 경우 이 아이콘은 결과를 더 일찍 표시합니다. run 메소드는 methodOne 메소드를 호출하고 methodOne 메소드는 methodTwo 메소드를 호출합니다.
methodOne 메소드는 기본형(int형)의 지역변수와 객체참조의 지역변수를 선언한다.
각 스레드는 methodOne 메서드를 실행할 때 해당 스레드 스택에 localVariable1 및 localVariable2의 자체 복사본을 생성합니다. 이 localVariable1은 서로 완전히 분리되어 해당 스레드 스택에만 유지됩니다. 한 스레드는 다른 스레드가 localVariable1에 적용한 변경 사항을 볼 수 없습니다.
methodOne 메서드를 실행하는 각 스레드는 자체 localVariable2 복사본도 생성합니다. 그러나 이러한 두 개의 서로 다른 localVariable2 복사본은 힙의 동일한 개체를 가리킵니다. 이 코드는 정적 변수를 통해 객체에 대한 참조를 가리키도록 localVariable2를 설정합니다. 정적 변수의 복사본은 하나만 있으며 이 복사본은 힙에 있습니다. 따라서 localVariable2의 두 복사본은 모두 동일한 인스턴스를 가리키게 됩니다. 이 MySharedObject도 힙에 저장됩니다. 위 그림의 객체 3과 동일합니다.
이 MySharedObject 클래스에는 두 개의 멤버 변수도 포함되어 있습니다. 멤버 변수 자체는 개체와 함께 힙에 저장됩니다. 이 두 멤버 변수는 두 개의 다른 Integer 개체를 가리킵니다. 이러한 Integer 개체는 위 그림의 개체 2 및 개체 4와 동일합니다.
또한 methodTwo 메서드가 localVariable1의 지역 변수를 생성하는 방법에 유의하세요. 이 지역 변수는 Integer 객체에 대한 참조입니다. 이 메소드는 새 Integer 인스턴스를 가리키도록 localVariable1 참조를 설정합니다. 이 localVariable1 참조는 실행 중인 methodTwo 메서드의 각 스레드 복사본에 저장됩니다. 인스턴스화된 두 개의 Integer 객체는 힙에 저장되지만 이 메소드가 실행될 때마다 새로운 Integer 객체가 생성되고 이 메소드를 실행하는 두 스레드는 별도의 Integer 인스턴스를 생성합니다. methodTwo 메소드 내부에 생성된 Integer 객체는 위 그림의 객체 1, 객체 5와 동일합니다.
또한 MySharedObject 클래스에 있는 long 유형의 두 멤버 변수는 기본 유형입니다. 이러한 변수는 멤버 변수이기 때문에 여전히 개체와 함께 힙에 저장됩니다. 스레드 스택에는 지역 변수만 저장됩니다.
하드웨어 메모리 아키텍처
현재 하드웨어 메모리 아키텍처는 내부 Java 메모리 모델과 약간 다릅니다. 하드웨어 메모리 아키텍처를 이해하는 것도 중요하며, Java 메모리 모델이 어떻게 작동하는지 이해하는 것이 도움이 됩니다. 이 섹션에서는 일반적인 하드웨어 메모리 프레임워크를 설명하고, 다음 섹션에서는 Java 메모리 모델이 어떻게 작동하는지 설명합니다.
다음은 현대 컴퓨터의 하드웨어 구조를 단순화한 다이어그램입니다.
현대 컴퓨터에는 CPU가 두 개 이상 있는 경우가 많습니다. 이러한 CPU 중 일부에는 다중 코어가 있을 수 있습니다. 중요한 점은 두 개 이상의 CPU가 있는 컴퓨터에서는 동시에 두 개 이상의 스레드가 실행될 수 있다는 것입니다. 각 CPU는 언제든지 하나의 스레드를 실행할 수 있습니다. Java 애플리케이션에서는 하나의 스레드가 각 CPU에서 동시에 실행될 수 있습니다.
각 CPU에는 본질적으로 CPU 메모리인 일련의 레지스터가 포함되어 있습니다. 이 CPU는 주 메모리보다 레지스터에서 더 빠르게 실행됩니다. 이는 CPU가 메인 메모리에 액세스하는 것보다 레지스터에 더 빠르게 액세스하기 때문입니다.
각 CPU에는 CPU 캐시를 위한 메모리 계층이 있을 수도 있습니다. 실제로 대부분의 최신 CPU에는 일정 크기의 캐시 메모리 계층이 있습니다. 이 CPU는 주 메모리보다 훨씬 빠르게 캐시 메모리 계층에 액세스하지만 내부 레지스터에 액세스하는 것만큼 빠르지는 않습니다. 결과적으로 이 CPU 캐시 메모리의 액세스 속도는 내부 레지스터와 메인 메모리 사이에 있습니다. 일부 CPU에는 여러 수준의 캐시(수준 1 및 수준 2)가 있을 수 있지만 Java 메모리 모델과 메모리의 상호 작용을 이해하기 위해 이를 아는 것은 중요하지 않습니다. CPU에 캐시 메모리 계층이 있을 수 있다는 것을 아는 것이 중요합니다.
컴퓨터에도 주 메모리 영역(RAM)이 있습니다. 모든 CPU는 이 주 메모리에 액세스할 수 있습니다. 이 주 메모리는 일반적으로 CPU의 캐시 메모리보다 큽니다.
대표적으로 CPU가 메인 메모리에 액세스해야 할 때 메인 메모리 부분을 CPU 캐시로 읽어옵니다. 캐시의 일부를 레지스터로 읽어온 다음 그곳에서 작업을 수행할 수도 있습니다. CPU가 결과를 다시 메인 메모리에 써야 할 때 내부 레지스터의 값을 캐시 메모리로 플러시하고 어느 시점에서 그 값을 메인 메모리로 플러시합니다.
캐시 메모리에 저장된 이러한 값은 CPU가 다른 것을 메인 메모리에 저장해야 할 때 메인 메모리로 플러시됩니다. 이 CPU 캐시는 때로는 메모리의 일부에 기록될 수도 있고 때로는 메모리의 일부가 플러시될 수도 있습니다. 매번 전체 캐시를 읽고 쓸 필요가 없습니다. 일반적으로 이 캐시는 "캐시 라인"이라고 하는 더 작은 메모리 블록에서 업데이트됩니다. 하나 이상의 캐시 라인을 캐시 메모리로 읽을 수 있으며, 하나 이상의 캐시 라인을 다시 주 메모리로 플러시할 수 있습니다.
Java 메모리 모델과 하드웨어 메모리 구조 간의 격차 해소
이미 언급했듯이 Java 메모리 모델과 하드웨어 메모리 구조는 다릅니다. 이 하드웨어 메모리 구조는 스레드 스택과 힙을 구분하지 않습니다. 하드웨어에서는 스레드 스택과 힙이 모두 주 메모리에 위치합니다. 다음 그림과 같이 스레드 스택 및 힙의 일부가 CPU 캐시 및 내부 CPU 레지스터에 나타날 수 있습니다.
객체 및 변수가 있는 경우 특정 문제 컴퓨터의 다양한 메모리 영역에 데이터를 저장할 수 있는 경우 발생할 수 있습니다. 두 가지 주요 문제는 다음과 같습니다.
공유 변수 업데이트에 대한 스레드 가시성
Race 조건을 읽을 때 공유 변수 가져오기, 확인 및 쓰기
이러한 문제는 다음 섹션에서 설명됩니다.
공유 객체의 가시성
2인 경우 또는 if 더 많은 스레드가 객체를 공유하고, 휘발성 선언이나 동기화를 적절하게 사용하지 않으면 한 스레드에서 업데이트된 공유 변수가 다른 스레드에 표시되지 않을 수 있습니다.
공유 객체가 처음에 메인 메모리에 저장되었다고 상상해 보세요. CPU에서 실행되는 스레드는 공유 객체를 CPU 캐시로 읽어옵니다. 여기에서는 공유 객체를 변경합니다. CPU 캐시가 주 메모리로 플러시되지 않는 한 이 공유 객체의 변경된 버전은 다른 CPU에서 실행되는 스레드에 표시되지 않습니다. 이런 식으로 각 스레드는 공유 객체의 자체 복사본으로 끝날 수 있으며 각 복사본은 다른 CPU 캐시에 위치합니다.
아래 다이어그램은 대략적인 상황을 보여줍니다. 왼쪽 CPU에서 실행되는 스레드는 이 공유 변수를 CPU 캐시에 복사하고 해당 값을 2로 변경합니다. count에 대한 업데이트가 아직 주 메모리로 다시 플러시되지 않았기 때문에 이 변경 사항은 올바른 CPU에서 실행 중인 다른 스레드에 표시되지 않습니다.
이 문제를 해결하려면 Java의 휘발성 키워드를 사용할 수 있습니다. 이 키워드는 주어진 변수가 주 메모리에서 직접 읽히고 업데이트될 때 주 메모리에 직접 쓰여지도록 보장합니다.
경합 조건
둘 이상의 스레드가 객체를 공유하고 둘 이상의 스레드가 공유 객체의 변수를 업데이트하는 경우 경쟁 조건 발생할 수 있습니다.
스레드 A가 공유 객체의 count 변수를 CPU 캐시로 읽는다고 상상해 보세요. 한편 스레드 B는 동일한 작업을 수행하지만 다른 CPU 캐시로 이동합니다. 이제 스레드 증가는 1씩 계산되고 스레드 B는 동일한 작업을 수행합니다. 이제 변수가 두 번 증가합니다.
이러한 증분을 순차적으로 수행하면 카운트 변수가 두 번 증가하고 원래 값을 기준으로 더하기 2가 메인 메모리에 기록됩니다.
그러면 두 증분이 제대로 동기화되지 않아 동시 실행이 발생하게 됩니다. 스레드 A 또는 스레드 B가 업데이트를 주 메모리에 기록하는지 여부에 관계없이 이 업데이트의 값은 2만큼 증가하지 않고 1만큼만 증가합니다.
이 다이어그램은 위에서 설명한 경쟁 조건의 문제를 보여줍니다.
이 문제를 해결하려면 Java 동기화 잠금을 사용할 수 있습니다. 동기화 잠금을 사용하면 언제든지 하나의 스레드만 코드의 중요 영역에 들어갈 수 있습니다. 또한 동기화 잠금은 모든 변수 액세스가 주 메모리에서 읽히도록 보장하며 스레드가 동기화된 코드 블록을 떠날 때 변수가 휘발성으로 선언되었는지 여부에 관계없이 업데이트된 모든 변수가 다시 주 메모리로 다시 플러시됩니다.
이상은 Java 메모리 모델에 대한 자세한 소개입니다. 더 많은 관련 내용은 PHP 중국어 홈페이지(www.php.cn)를 참고해주세요!