수색…


비고

Java 메모리 모델은, JLS의 섹션으로, 어느 thread가 다른 thread에 의한 메모리 기입의 효과를 보증하는 조건을 지정합니다. 최근 버전의 관련 섹션은 "JLS 17.4 메모리 모델"( Java 8 , Java 7 , Java 6 )

Java 5의 Java 메모리 모델을 대대적으로 개편하여 (다른 것들 중에서) volatilevolatile 방식을 변경했습니다. 그 이후로 메모리 모델은 본질적으로 변하지 않았습니다.

기억 모델에 대한 동기 부여

다음 예제를 고려하십시오.

public class Example {
    public int a, b, c, d;
    
    public void doIt() {
       a = b + 1;
       c = d + 1;
    }
}

이 클래스가 단일 스레드 응용 프로그램으로 사용되는 경우, 관찰 가능한 동작은 예상대로 정확하게 수행됩니다. 예를 들면 :

public class SingleThreaded {
    public static void main(String[] args) {
        Example eg = new Example();
        System.out.println(eg.a + ", " + eg.c);
        eg.doIt();
        System.out.println(eg.a + ", " + eg.c);
    }
}

출력됩니다 :

0, 0
1, 1

"main"쓰레드가 알 수 있는 한, main() 메소드와 doIt() 메소드의 문장은 소스 코드에 쓰여진 순서대로 실행된다. 이것은 Java 언어 사양 (JLS)의 명확한 요구 사항입니다.

이제 다중 스레드 응용 프로그램에서 사용되는 동일한 클래스를 고려하십시오.

public class MultiThreaded {
    public static void main(String[] args) {
        final Example eg = new Example();
        new Thread(new Runnable() {
            public void run() {
                while (true) {
                    eg.doIt();
                }
            }
        }).start();
        while (true) {
            System.out.println(eg.a + ", " + eg.c);
        }
    }
}

무엇이 인쇄됩니까?

실제로, JLS에 의하면, 이것이 인쇄 될지 예측할 수 없습니다.

  • 0, 0 으로 시작하는 몇 줄을 보게 될 것입니다.
  • 그런 다음 N, N 또는 N, N + 1 과 같은 선을 보게됩니다.
  • N + 1, N 과 같은 행을 볼 수 있습니다.
  • 이론 상으로는 0, 0 라인이 영원히 계속된다는 것을 볼 수도 있습니다 1 .

1 - 실제로 println 문이 있으면 일정한 동기화 및 메모리 캐시 플러시가 발생할 수 있습니다. 이는 위의 행동을 유발할 수있는 효과 중 일부를 숨길 수 있습니다.

그러면 어떻게 설명 할 수 있을까요?

과제 재정렬

예기치 않은 결과에 대한 한 가지 가능한 설명은 JIT 컴파일러가 doIt() 메서드에서 할당 순서를 변경했기 때문입니다. JLS에서는, 문장 이 현재의 thread 의 관점으로부터 순서 실행 되도록 ( 듯이) 요구합니다. 이 경우, doIt() 메소드의 코드에서는이 두 문장의 (가상의) 재정렬 효과를 관찰 할 수 없다. 이것은 JIT 컴파일러가 그렇게 할 수 있음을 의미합니다.

왜 그렇게했을까요?

전형적인 현대의 하드웨어에서, 기계 명령은 일련의 명령이 다른 단계에 있도록하는 명령 파이프 라인을 사용하여 실행됩니다. 명령 실행의 일부 단계는 다른 단계보다 시간이 오래 걸리고 메모리 작업은 시간이 오래 걸리는 경향이 있습니다. 스마트 컴파일러는 오버랩의 양을 최대화하기 위해 명령을 순서화하여 파이프 라인의 명령 처리량을 최적화 할 수 있습니다. 이로 인해 명령문의 일부가 순서없이 실행될 수 있습니다. JLS 에서는, 현재의 thread의 관점으로부터 계산의 결과에 영향을 미치지 않는, 이것을 제공합니다.

메모리 캐시의 효과

두 번째 가능한 설명은 메모리 캐싱의 효과입니다. 고전적인 컴퓨터 아키텍처에서 각 프로세서에는 작은 레지스터 세트와 더 많은 양의 메모리가 있습니다. 레지스터에 대한 액세스는 주 메모리에 대한 액세스보다 훨씬 빠릅니다. 현대 아키텍처에서는 레지스터보다 느리지 만 주 메모리보다 빠른 메모리 캐시가 있습니다.

컴파일러는 변수의 복사본을 레지스터 또는 메모리 캐시에 유지하려고 시도함으로써이 문제를 악용합니다. 변수가 메인 메모리에 플러시 될, 또는 메모리에서 읽을 필요가 없습니다 필요하지 않는 경우,이 일을하지 상당한 성능 이점이있다. JLS가 메모리 조작을 다른 스레드에서 볼 필요가없는 경우 Java JIT 컴파일러는 주 메모리 읽기 및 쓰기를 강제하는 "읽기 장벽"및 "쓰기 장벽"지침을 추가하지 않을 가능성이 있습니다. 다시 한번, 이렇게하는 성능 이점은 중요합니다.

적절한 동기화

지금까지 우리는 JLS가 JIT 컴파일러로 하여금 메모리 조작을 재정리하거나 피함으로써 단일 스레드 코드를 더 빠르게 만드는 코드를 생성하도록했습니다. 그러나 다른 스레드가 주 메모리에서 (공유 된) 변수의 상태를 관찰 할 수 있다면 어떻게 될까요?

그 대답은 다른 스레드가 Java 문에 대한 코드 순서에 따라 불가능한 것으로 보이는 변수 상태를 관찰 할 가능성이 있다는 것입니다. 이에 대한 해결책은 적절한 동기화를 사용하는 것입니다. 세 가지 주요 접근법은 다음과 같습니다.

  • 원시 mutex와 synchronized 구문을 사용합니다.
  • volatile 변수 사용.
  • 보다 높은 수준의 동시성 지원 사용; 예를 들어, java.util.concurrent 패키지의 클래스

그러나이 경우에도 동기화가 필요한 부분과 신뢰할 수있는 영향을 이해하는 것이 중요합니다. 이것은 Java 메모리 모델이 들어있는 곳입니다.

메모리 모델

Java 메모리 모델은, JLS의 섹션으로, 어느 thread가 다른 thread에 의한 메모리 기입의 효과를 보증하는 조건을 지정합니다. 메모리 모델은 공정한 정도의 공식적인 엄격함으로 지정되며, (결과적으로) 이해하기 위해서는 세밀하고주의 깊은 독해가 필요합니다. 그러나 기본 원칙은 특정 구조가 한 스레드에 의한 변수 쓰기와 다른 스레드에 의한 동일한 변수의 후속 읽기 사이에 "발생 전"관계를 생성한다는 것입니다. "happen before"관계가 존재하면 JIT 컴파일러는 읽기 작업에서 쓰기로 작성된 값을 확인하는 코드를 생성 해야 합니다.

이것으로 무장하면 Java 프로그램의 메모리 일관성을 추론하고 이것이 모든 실행 플랫폼에서 예측 가능하고 일관성있게 유지 될지 결정할 수 있습니다.

일이 - 관계 전에

(다음은 Java Language Specification이 말하는 단순화 된 버전입니다. 더 깊은 이해를 위해서는 사양 자체를 읽어야합니다.)

Happens-Before 관계는 우리가 메모리 가시성을 이해하고 추론 할 수있게 해주는 메모리 모델의 일부입니다. JLS가 말한 것처럼 ( JLS 17.4.5 ) :

"두 가지 행동 은 일찍 발생 하는 관계에 의해 명령 될 수 있습니다. 한 행동이 다른 행동 보다 먼저 발생 하면 첫 번째 행동 은 두 번째 행동 전에 나타나고 두 번째 행동 전에 주문됩니다."

이것은 무엇을 의미 하는가?

행위

위 인용문이 참조하는 동작은 JLS 17.4.2에 지정되어 있습니다. 사양에 정의 된 5 가지 종류의 동작이 있습니다.

  • 읽기 : 비 휘발성 변수를 읽습니다.

  • 쓰기 : 비 휘발성 변수 쓰기.

  • 동기화 작업 :

    • 휘발성 읽기 : 휘발성 변수를 읽습니다.

    • 휘발성 쓰기 : 휘발성 변수 쓰기.

    • 자물쇠. 모니터 잠그기

    • 터놓다. 모니터 잠금 해제.

    • 스레드의 (합성) 최초 및 마지막 동작.

    • 스레드를 시작하거나 스레드가 종료되었음을 감지하는 작업입니다.

  • 외부 행동. 프로그램을 실행하는 환경에 따라 결과가 달라질 수 있습니다.

  • 스레드 발산 조치. 이 모델은 특정 종류의 무한 루프의 동작을 모델링합니다.

프로그램 순서 및 동기화 순서

이 두 순서 ( JLS 17.4.3JLS 17.4.4 )는 Java에서 명령문 실행을 제어합니다.

프로그램 순서는 단일 스레드 내에서 명령문 실행 순서를 설명합니다.

동기화 순서는 동기화로 연결된 두 명령문에 대한 명령문 실행 순서를 설명합니다.

  • 모니터의 잠금 해제 작업은 해당 모니터의 모든 후속 잠금 작업 과 동기화 됩니다.

  • 휘발성 변수에 대한 쓰기는 모든 스레드에 의해 동일한 변수의 모든 후속 읽기 와 동기화 됩니다.

  • 스레드를 시작하는 작업 (즉, Thread.start() 호출)은 시작하는 스레드의 첫 번째 작업 (즉 스레드의 run() 메서드 호출)과 동기화 됩니다.

  • 필드의 기본 초기화는 모든 스레드의 첫 번째 작업 과 동기화 됩니다. 이 설명에 대해서는 JLS를 참조하십시오.

  • 스레드의 마지막 동작은 종료를 감지 한 다른 스레드의 모든 동작 과 동기화 됩니다. true 를 리턴하는 join() 호출 또는 isTerminated() 호출의 리턴 isTerminated() .

  • 한 스레드가 다른 스레드를 인터럽트하면 첫 번째 스레드의 인터럽트 호출이 다른 스레드가 스레드가 중단되었음을 감지 한 지점 과 동기화 됩니다.

주문 전 발생

이 순서 붙이고 ( JLS 17.4.5 )는, 메모리 기입이, 후속의 메모리 독해로부터 가시가 될지 어떨지를 결정합니다.

보다 구체적으로는, 변수의 읽기 v 에 쓰기를 관찰 보장 v 및 경우 만 write(v) 발생 - 전 read(v) 와 아무런 간섭 쓰기가 없습니다 v . 중간에 쓰기가있는 경우 read(v) 는 이전 것보다 결과를 볼 수 있습니다.

발생 전 주문을 정의하는 규칙은 다음과 같습니다.

  • 규칙 # 1 이전의 일 - x와 y가 같은 쓰레드의 액션이고 x가 프로그램 순서 에서 y보다 앞서면 x는 y 이전에 발생한다 .

  • Rule # 2 이전의 일이다 - 객체 생성자의 끝에서부터 객체의 finalizer 시작까지 일어나는 일이있다.

  • 규칙 3 이전에 일어난 일 - 액션 x가 후속 액션 y 와 동기화 되면 x 가 일어나기 전에 y 가 발생합니다 .

  • 규칙 # 4 이전에 발생 - x 가 발생한 경우 - y와 y 가 발생하기 전 - z 이전에는 x 가 발생 - z 이전 .

또한 Java 표준 라이브러리의 다양한 클래스는 발생 가능 관계를 정의하는 것으로 지정됩니다. 이것을 보장하는 방법을 정확히 알 필요없이 어떻게 든 일어날 수 있다는 의미로 해석 할 수 있습니다.

일어남 - 추론하기 전에 일부 예제에 적용됨

우리는 후속 읽기에 쓰기가 표시 되는지 확인 하기 위해 사전에 추론을 적용하는 방법을 보여주는 몇 가지 예를 제시합니다.

단일 스레드 코드

예상대로, 쓰기는 단일 스레드 프로그램의 후속 읽기에서 항상 볼 수 있습니다.

public class SingleThreadExample {
    public int a, b;
    
    public int add() {
       a = 1;         // write(a)
       b = 2;         // write(b)
       return a + b;  // read(a) followed by read(b)
    }
}

Rule by Happen-Before 규칙 1 :

  1. write(a) 동작은 write(b) 동작 이전에 발생 합니다.
  2. write(b) 액션은 read(a) 액션 이전에 발생 합니다.
  3. read(a) 액션이 발생-전에 read(a) 작업입니다.

일을 통해 규칙 # 4 :

  1. write(a) 발생 전 - write(b) AND write(b) 이 발생하기 전에 - read(a) 한다고 해석 write(a) 이 발생하기 전에 - read(a) .
  2. write(b) 이 발생하기 전에 - read(a) AND read(a) 발생 전 - read(b) 한다고 해석 write(b) 발생 전 - read(b) .

합산:

  1. write(a) 발생-전 read(a) 관계가 있음을 의미 a + b 문이의 정확한 값을 참조 보장 a .
  2. write(b) 발생-전 read(b) 의 관계가 있음을 의미 a + b 문이의 정확한 값을 참조 보장 b .

2 개의 스레드가있는 예제에서의 'volatile'동작

다음 예제 코드를 사용하여 메모리 모델의 '휘발성'에 대한 몇 가지 함의를 탐색합니다.

public class VolatileExample {
    private volatile int a;
    private int b;         // NOT volatile
    
    public void update(int first, int second) {
       b = first;         // write(b)
       a = second;         // write-volatile(a)
    }

    public int observe() {
       return a + b;       // read-volatile(a) followed by read(b)
    }
}

먼저, 2 개의 쓰레드가 관련된 다음 명령문의 순서를 생각해 보자 :

  1. VolatileExample 의 단일 인스턴스가 생성됩니다. 이 전화를 ve ,
  2. ve.update(1, 2) 가 한 스레드에서 호출되고
  3. ve.observe() 가 다른 스레드에서 호출됩니다.

Rule by Happen-Before 규칙 1 :

  1. write(a) 동작은 volatile-write(a) 동작 이전에 발생 합니다.
  2. volatile-read(a) 동작은 read(b) 동작 이전에 발생 합니다.

Rule by Happen-Before 규칙 2 :

  1. 첫 번째 스레드의 volatile-write(a) 동작은 두 번째 스레드의 volatile-read(a) 동작 이전에 발생 합니다.

일을 통해 규칙 # 4 :

  1. 첫 번째 스레드의 write(b) 동작은 두 번째 스레드의 read(b) 동작 이전에 발생 합니다.

즉,이 특정 시퀀스에 대해 우리는 두 번째 스레드가 첫 번째 스레드가 만든 비 휘발성 변수 b 대한 업데이트를 보게됩니다. 그러나, 그것은 또한에 할당하면 분명해야한다입니다 update 방법은 주위에 다른 방법이었다, 또는 observe() 메서드는 변수를 읽을 b 전에 a 한 후 발생-전에 체인이 파괴 될 것이다. 만약 체인은 파괴 될 것이다 volatile-read(a) 제 글은 후속 아니었다 volatile-write(a) 첫 번째 스레드이다.

체인이 끊어지면 observe() 에 올바른 값인 b 가 표시된다는 보장 이 없습니다.

3 개의 스레드로 휘발성

앞의 예제에 세 번째 스레드를 추가한다고 가정 해 보겠습니다.

  1. VolatileExample 의 단일 인스턴스가 생성됩니다. 이 전화를 ve ,
  2. 두 스레드가 update 호출합니다.
    • ve.update(1, 2) 가 하나의 스레드에서 호출됩니다.
    • ve.update(3, 4) 가 두 번째 스레드에서 호출되고,
  3. ve.observe() 는 이후에 세 번째 스레드에서 호출됩니다.

이를 완전히 분석하려면 스레드 1과 스레드 2에서 명령문의 가능한 모든 인터 리빙을 고려해야합니다. 대신, 우리는 두 가지를 고려할 것입니다.

시나리오 # 1 - update(1, 2)update(3,4) 시퀀스를 얻는다 고 가정합니다.

write(b, 1), write-volatile(a, 2)     // first thread
write(b, 3), write-volatile(a, 4)     // second thread
read-volatile(a), read(b)             // third thread

이 경우, write(b, 3) 에서 read(b) 까지 연속적으로 발생 하지 않는 chain이 있음을 쉽게 알 수 있습니다. 또한 b 대한 중간 기록이 없습니다. 따라서이 시나리오에서 세 번째 스레드는 b 가 값 3 을 갖는 것으로 보증됩니다.

시나리오 # 2 - update(1, 2)update(3,4) 중복되고 다음과 같이 항목이 인터리빙된다고 가정합니다.

write(b, 3)                           // second thread
write(b, 1)                           // first thread
write-volatile(a, 2)                  // first thread
write-volatile(a, 4)                  // second thread
read-volatile(a), read(b)             // third thread

이제 write(b, 3) 에서 read(b) 까지 일어난 체인이 있지만 다른 스레드가 수행하는 중간 write(b, 1) 액션이 있습니다. 이것은 read(b) 가 어떤 값을 read(b) 확신 할 수 없다는 것을 의미합니다.

(옆으로 : 이는 매우 제한된 상황을 제외하고는 비 휘발성 변수의 가시성을 보장하기 위해 volatile 에 의존 할 수 없음을 보여줍니다.)

메모리 모델을 이해할 필요가 없게하는 방법

메모리 모델은 이해하기 어렵고, 적용하기 어렵습니다. 다중 스레드 코드의 정확성에 대해 추론해야하지만 유용하게 쓰는 모든 다중 스레드 응용 프로그램에 대해이 추론을 수행하지 않으려는 경우 유용합니다.

자바에서 동시 코드를 작성할 때 다음과 같은 원칙을 채택한다면, 사전에 추론 하기 위한 필요성을 크게 피할 수 있습니다.

  • 가능하면 불변의 데이터 구조를 사용하십시오. 올바르게 구현 된 불변 클래스는 스레드로부터 안전하며, 다른 클래스와 함께 사용하면 스레드 안전 문제가 발생하지 않습니다.

  • "안전하지 않은 출판물"을 이해하고 피하십시오.

  • 기본 mutex 또는 Lock 객체를 사용하여 스레드 안전성이 필요한 변경 가능한 객체의 상태에 대한 액세스를 동기화합니다 1 .

  • 스레드 관리를 직접 만들지 말고 Executor / ExecutorService 또는 fork join 프레임 워크를 사용하십시오.

  • wait / notify / notifyAll를 직접 사용하는 대신 고급 잠금, 세마포어, 래치 및 장벽을 제공하는 java.util.concurrent 클래스를 사용하십시오.

  • 비 동시성 콜렉션의 외부 동기화보다는 맵, 세트,리스트, 큐 및 큐의 java.util.concurrent 버전을 사용하십시오.

일반적인 원칙은 "자신 만의"동시성을 구현하는 대신 Java의 내장형 동시성 라이브러리를 사용하는 것입니다. 제대로 사용하면 작업에 의지 할 수 있습니다.


1 - 모든 객체가 스레드로부터 안전해야하는 것은 아닙니다. 예를 들어 객체가 하나의 스레드에서만 액세스 할 수 있도록 스레드가 제한되어 있으면 스레드 안전이 적합하지 않습니다.



Modified text is an extract of the original Stack Overflow Documentation
아래 라이선스 CC BY-SA 3.0
와 제휴하지 않음 Stack Overflow