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


205

오늘 실험실에서 민감한 작업이 완전히 잘못되었습니다. 전자 현미경의 액츄에이터가 그 경계를 넘어 섰고, 일련의 사건 이후 나는 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을 사용하고 있습니다.


41
12 마일의 장비? 어떻게 될 수 있는지 궁금합니다 ... 왜 빈 동기화 블록을 사용하고 있습니까?
Martin V.

84
이것은 원격 스레드 안전하지도 않습니다.
매트 볼

8
주목할만한 점 : final한정자 (생성 된 바이트 코드에 영향을 미치지 않음)를 필드에 추가 x하고 y버그를 "해결"합니다. 바이트 코드에는 영향을 미치지 않지만 필드에 플래그가 지정되어있어 이것이 JVM 최적화의 부작용이라고 생각하게됩니다.
Niv Steingarten

9
@Eugene : 그것은해야 하지 끝납니다. 문제는 "왜 끝나는가?"입니다. Point p를 만족 하는 A 가 구성되면 p.x+1 == p.y, 참조 는 폴링 스레드로 전달됩니다. 결국 폴링 스레드는 Point수신 한 조건 중 하나에 대해 조건이 충족되지 않는다고 생각하기 때문에 종료하기로 결정 하지만 콘솔 출력은 해당 조건이 만족되어야 함을 표시합니다. volatile여기가 없다는 것은 단순히 폴링 스레드가 멈출 수 있음을 의미하지만 여기서 분명히 문제가되지는 않습니다.
Erma K. Pizarro

21
@JohnNicholas : 실제 코드 (분명히 그렇지는 않음)는 100 % 테스트 적용 범위와 수천 가지 테스트를 거쳤으며,이 중 다수는 수천 가지의 다양한 순서와 순열로 테스트했습니다. JIT / 캐시 / 스케줄러. 실제 문제는이 코드를 작성한 개발자가 객체를 사용하기 전에 구성이 발생하지 않는다는 것을 알지 못했다는 것입니다. 빈 synchronized을 제거하면 어떻게 버그가 발생하지 않습니까? 결정적인 방식으로이 동작을 재현 할 수있는 코드를 찾을 때까지 무작위로 코드를 작성해야했기 때문입니다.

답변:


140

분명히 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로 처리 할 수 ​​있습니다 (행동을 재현하기 위해 포함 시켰습니다).


4
확실하지 않지만 메모리 장벽을 피하면서 x와 y를 final로 만드는 효과는 없습니까?
Michael Böckling

3
더 단순한 설계는 시공시 변형을 테스트하는 불변의 점 객체입니다. 따라서 위험한 구성을 게시 할 위험이 없습니다.
Ron

@BuddyCasino 네 맞아요-추가했습니다. 솔직히 말해서 3 개월 전에 전체 토론을 기억하지 못합니다 (최종 의견을 의견에 제안했기 때문에 왜 옵션으로 포함하지 않았는지 확실하지 않습니다).
assylias

2
불변성 자체가 안전한 게시를 보장하지는 않습니다 (xy가 개인용이지만 게터 만 노출 된 경우에도 동일한 게시 문제가 여전히 존재합니다). final 또는 volatile은이를 보증합니다. 나는 휘발성보다 최종을 선호합니다.
Steve Kuo

@SteveKuo 불변성에는 final이 필요합니다. 최종적으로는 얻을 수있는 최선의 방법은 동일한 의미가없는 효과적인 불변성입니다.
assylias

29

때문에 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);
}

2
예, 또한 모든 것을 둘러 쌀 수 있습니다. 너의 요점이 뭐야?
Dog

의 사용에 대한 추가 설명을 추가했습니다 volatile.
Ed Plese

19

일반 메모리, '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 모델이 더 강력하다는 것을 지적합니다. ).


2
이것은 내 의견으로는 가장 좋은 설명입니다. 고마워요!
skyde

1
@skyde 그러나 휘발성의 의미론에는 잘못되었습니다. 휘발성 변수는 휘발성 변수를 읽을 때 휘발성 변수의 최신 사용 가능 쓰기와 이전 쓰기를 볼 수 있습니다. 이 경우, currentPos변동 이있는 경우, 지정은 currentPos오브젝트 자체가 변동 적이 지 않더라도 오브젝트와 멤버를 안전하게 공개 합니다.
assylias

글쎄, 나는 JLS가 휘발성이 다른 일반적인 읽기 및 쓰기와 장벽을 형성했다는 것을 어떻게 정확하게 보장했는지 알 수 없다고 말하고 있었다. 기술적으로는, 나는 틀릴 수 없다;). 메모리 모델의 경우 순서가 보장되지 않으며 다른 방법보다 잘못 (아직 안전), 잘못 및 안전하지 않은 것으로 가정하는 것이 좋습니다. 휘발성이 그러한 보증을 제공한다면 좋습니다. JLS의 17 장에서 제공하는 방법을 설명 할 수 있습니까?
paulj

2
간단히 말해서 Point currentPos = new Point(x, y)(w1) this.x = x, (w2) this.y = y및 (w3)의 3 가지 쓰기가 있습니다 currentPos = the new point. 프로그램 순서는 hb (w1, w3) 및 hb (w2, w3)를 보장합니다. 나중에 프로그램에서 (r1)을 읽으십시오 currentPos. currentPos휘발성이 아닌 경우 r1과 w1, w2, w3 사이에 hb가 없으므로 r1은 이들 중 어느 것도 관찰 할 수 있습니다. 휘발성에서는 hb (w3, r1)을 소개합니다. 그리고 hb 관계는 전이 적이므로 hb (w1, r1) 및 hb (w2, r1)도 소개합니다. 이것은 실제 Java Concurrency (3.5.3. 안전한 게시 숙어)에 요약되어 있습니다.
assylias

2
아, 만약 hb가 그런 식으로 전이된다면, 그것은 충분히 강력한 '장벽'입니다. 나는 JLS의 17.4.5가 hb가 그 속성을 갖도록 정의한다고 결정하는 것은 쉽지 않습니다. 17.4.5의 시작 부분에 주어진 속성 목록에는 확실하지 않습니다. 전이 폐쇄는 일부 설명이 끝난 후에 만 ​​더 언급됩니다! 어쨌든, 알게되어 반갑습니다. :). 참고 : assylias의 의견을 반영하여 답변을 업데이트하겠습니다.
paulj

-2

객체를 사용하여 쓰기와 읽기를 동기화 할 수 있습니다. 그렇지 않으면 다른 사람들이 이전에 말했듯이 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);
    }
}

실제로 이것은 작업을 수행합니다. 첫 번째 시도에서 나는 읽기를 동기화 된 블록 안에 넣었지만 나중에 실제로 필요하지 않다는 것을 깨달았습니다.
Germano Fronza

1
-1 JVM sem이 공유되지 않았 음을 증명 하고 동기화 된 명령문을 no-op로 처리 할 수 있습니다 .이 문제를 해결한다는 사실은 행운입니다.
assylias

4
나는 멀티 스레드 프로그래밍이 싫어서 운이 너무 많아서 너무 많은 것들이 작동합니다.
Jonathan Allen

-3

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 값 하나만 사용됩니다.


currentPos는 한 번만 읽으며 그 값은 p로 복사됩니다. p는 두 번 읽지 만 항상 같은 위치를 가리 킵니다.
Jonathan Allen
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.