[java] 이 Java 프로그램이 왜 그렇게해서는 안되지만 종료되지 않습니까?

오늘 실험실에서 민감한 작업이 완전히 잘못되었습니다. 전자 현미경의 액츄에이터가 그 경계를 넘어 섰고, 일련의 사건 이후 나는 1200 만 달러의 장비를 잃었습니다. 결함이있는 모듈의 40K 라인을 좁혔습니다.

import java.util.*;

class A {
    static Point currentPos = new Point(1,2);
    static class Point {
        int x;
        int y;
        Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }
    public static void main(String[] args) {
        new Thread() {
            void f(Point p) {
                synchronized(this) {}
                if (p.x+1 != p.y) {
                    System.out.println(p.x+" "+p.y);
                    System.exit(1);
                }
            }
            @Override
            public void run() {
                while (currentPos == null);
                while (true)
                    f(currentPos);
            }
        }.start();
        while (true)
            currentPos = new Point(currentPos.x+1, currentPos.y+1);
    }
}

내가 얻는 출력의 일부 샘플 :

$ java A
145281 145282
$ java A
141373 141374
$ java A
49251 49252
$ java A
47007 47008
$ java A
47427 47428
$ java A
154800 154801
$ java A
34822 34823
$ java A
127271 127272
$ java A
63650 63651

여기에는 부동 소수점 산술이 없으며 부호있는 정수가 Java의 오버플로에서 잘 작동한다는 것을 알고 있으므로이 코드에는 아무런 문제가 없다고 생각합니다. 그러나 프로그램이 종료 조건에 도달하지 않았 음을 나타내는 출력에도 불구하고 (이것은 모두 도달 한 종료 상태에 도달 하고 도달하지?). 왜?


일부 환경에서는이 문제가 발생하지 않습니다. 64 비트 Linux 에서 OpenJDK 6을 사용하고 있습니다.



답변

분명히 currentPos에 대한 쓰기는 읽기 전에 발생하지 않지만 어떻게 이것이 문제가 될 수 있는지 알지 못합니다.

currentPos = new Point(currentPos.x+1, currentPos.y+1);기본값을 xand y(0)에 쓴 다음 생성자에 초기 값을 쓰는 것을 포함하여 몇 가지 작업 을 수행합니다. 객체가 안전하게 게시되지 않기 때문에 컴파일러 / JVM에서이 4 개의 쓰기 작업을 자유롭게 다시 정렬 할 수 있습니다.

따라서 읽기 스레드의 관점에서 x새 값 으로 읽지 만 y기본값은 0 으로 읽는 것이 합법적 입니다. println명령문에 도달 할 때까지 (동기화되어 읽기 조작에 영향을주는) 변수는 초기 값을 가지며 프로그램은 예상 값을 인쇄합니다.

객체를 효과적으로 변경할 수 없기 때문에 안전한 게시를 보장하는 currentPos것으로 표시 volatile-실제 사용 사례에서 생성 후 객체가 변형되면 volatile보장이 충분하지 않으며 불일치 한 객체를 다시 볼 수 있습니다.

또는 Point을 사용하지 않아도 안전한 게시를 보장 하는 불변을 만들 수 있습니다 volatile. 불변성을 달성하려면 간단히 표시 x하고 y마무리하면됩니다.

참고로 이미 언급했듯이 synchronized(this) {}JVM에서 no-op로 처리 할 수 ​​있습니다 (행동을 재현하기 위해 포함 시켰습니다).


답변

때문에 currentPos실 외부에서 변경되는 것이으로 표시한다 volatile:

static volatile Point currentPos = new Point(1,2);

휘발성이 없으면 스레드가 기본 스레드에서 수행중인 currentPos의 업데이트를 읽도록 보장되지 않습니다. 따라서 currentPos에 대해 새 값이 계속 작성되지만 성능상의 이유로 스레드가 이전 캐시 버전을 계속 사용합니다. 하나의 스레드 만 currentPos를 수정하므로 잠금 없이도 성능을 향상시킬 수 있습니다.

비교 및 후속 표시에 사용하기 위해 스레드 내에서 한 번만 값을 읽으면 결과가 크게 달라집니다. 나는 다음 작업을 수행 할 때 x항상 표시 1하고이 y사이에서 변화 0와 일부 대형 정수입니다. 나는이 시점에서 그것의 행동이 volatile키워드 없이 정의되지 않았다고 생각하고 코드의 JIT 컴파일이 이와 같이 작동하는 데 기여할 수 있습니다. 또한 빈 synchronized(this) {}블록을 주석 처리 하면 코드도 작동하며 잠금으로 인해 충분한 지연이 발생 currentPos하고 필드가 캐시에서 사용되지 않고 다시 읽혀지기 때문입니다.

int x = p.x + 1;
int y = p.y;

if (x != y) {
    System.out.println(x+" "+y);
    System.exit(1);
}


답변

일반 메모리, ‘currentpos’참조, Point 객체 및 그 뒤에있는 필드가 동기화없이 두 스레드간에 공유됩니다. 따라서 메인 스레드에서이 메모리에 발생하는 쓰기와 생성 된 스레드에서 읽기 사이에 정의 된 순서는 없습니다 (T라고 함).

메인 스레드는 다음 쓰기를 수행합니다 (점의 초기 설정을 무시하면 px 및 py가 기본값을 갖습니다).

  • px
  • 파이
  • currentpos에

동기화 / 배리어 측면에서 이러한 쓰기에는 특별한 것이 없기 때문에 런타임은 T 스레드가 임의 순서로 발생하는 것을 볼 수 있습니다 (물론 메인 스레드는 항상 프로그램 순서에 따라 순서대로 쓰기 및 읽기를 읽음). T의 판독 값 사이의 모든 지점에서

그래서 T는하고 있습니다 :

  1. p에 currentpos를 읽는다
  2. px와 py를 읽습니다 (어느 순서로든)
  3. 비교하고 지점을 가져 가라.
  4. px와 py (순서)를 읽고 System.out.println을 호출하십시오.

main의 쓰기와 T의 읽기 사이에 순서 관계가 없기 때문에 T가 currentpos.y 또는 currentpos.x에 쓰기 전에 mainpos의 currentpos 대한 쓰기를 볼 수 있기 때문에 결과를 생성 할 수있는 몇 가지 방법이 있습니다 .

  1. x 쓰기가 발생하기 전에 currentpos.x를 먼저 읽습니다. 0을 얻은 다음 y 쓰기가 발생하기 전에 currentpos.y를 읽습니다. 0을 비교합니다. evals를 true와 비교하십시오. 쓰기는 T에 표시됩니다. System.out.println이 호출됩니다.
  2. x 쓰기가 발생한 후 currentpos.x를 먼저 읽은 다음 y 쓰기가 발생하기 전에 currentpos.y를 읽습니다. 0을 얻습니다. evals를 true와 비교하십시오. 쓰기는 T 등에 표시됩니다.
  3. y 쓰기가 발생하기 전에 currentpos.y를 먼저 읽고 (0) x 쓰기 후에 currentpos.x를 읽고 true로 바꿉니다. 기타

등등 … 여기에 여러 가지 데이터 레이스가 있습니다.

여기에 결함이있는 가정은이 줄의 결과가 스레드를 실행하는 프로그램 순서로 모든 스레드에서 볼 수 있다고 생각한다고 생각합니다.

currentPos = new Point(currentPos.x+1, currentPos.y+1);

Java는 그러한 보증을하지 않습니다 (성능이 끔찍할 수도 있습니다). 프로그램이 다른 스레드의 읽기와 관련된 쓰기 순서를 보장해야하는 경우 추가해야합니다. 다른 사람들은 x, y 필드를 최종으로 만들거나 currentpos를 휘발성으로 만들 것을 제안했습니다.

  • x, y 필드를 final로 설정하면 Java는 모든 스레드에서 생성자가 리턴하기 전에 해당 값의 쓰기가 발생 함을 보증합니다. 따라서 currentpos에 대한 할당이 생성자 뒤에 있기 때문에 T 스레드는 올바른 순서로 쓰기를 볼 수 있습니다.
  • currentpos를 휘발성으로 만들면 Java는 이것이 다른 동기화 지점에서 전체 순서로 정렬되는 동기화 지점임을 보증합니다. main에서와 같이 x와 y에 대한 쓰기는 currentpos에 쓰기 전에 발생해야하며, 다른 스레드에서 currentpos에 대한 읽기는 이전에 발생한 x, y의 쓰기도 참조해야합니다.

final을 사용하면 필드를 변경할 수 없으므로 값을 캐시 할 수 있다는 이점이 있습니다. 휘발성을 사용하면 currentpos의 모든 쓰기 및 읽기에서 동기화가 수행되어 성능이 저하 될 수 있습니다.

자세한 내용은 Java 언어 사양 17 장을 참조하십시오. http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html

(초기 답변은 JLS가 휘발성이 충분하다는 것을 확신하지 못했기 때문에 메모리 모델이 약한 것으로 가정했습니다. assylias의 의견을 반영하도록 편집 된 답변은 Java 모델이 더 강력하다는 것을 지적합니다. ).


답변

객체를 사용하여 쓰기와 읽기를 동기화 할 수 있습니다. 그렇지 않으면 다른 사람들이 이전에 말했듯이 currentPos에 대한 쓰기는 두 번의 읽기 p.x + 1 및 py의 중간에 발생합니다.

new Thread() {
    void f(Point p) {
        if (p.x+1 != p.y) {
            System.out.println(p.x+" "+p.y);
            System.exit(1);
        }
    }
    @Override
    public void run() {
        while (currentPos == null);
        while (true)
            f(currentPos);
    }
}.start();
Object sem = new Object();
while (true) {
    synchronized(sem) {
        currentPos = new Point(currentPos.x+1, currentPos.y+1);
    }
}


답변

currentPos에 두 번 액세스하고 있으며이 두 액세스간에 업데이트되지 않는다는 보장은 없습니다.

예를 들면 다음과 같습니다.

  1. x = 10, y = 11
  2. 작업자 스레드는 px를 10으로 평가합니다.
  3. 메인 스레드가 업데이트를 실행합니다. 이제 x = 11 및 y = 12
  4. 작업자 스레드는 py를 12로 평가합니다.
  5. 워커 스레드는 10 + 1! = 12이므로 인쇄하고 종료합니다.

본질적으로 두 가지를 비교하고 있습니다. 포인트를 있습니다.

currentPos를 휘발성으로 만들더라도 작업자 스레드가 두 번의 별도 읽기를 수행하므로이를 방지 할 수는 없습니다.

추가

boolean IsValid() { return x+1 == y; }

당신의 포인트 클래스에 방법. 이렇게하면 x + 1 == y를 확인할 때 currentPos 값 하나만 사용됩니다.


답변