[java] 정적 초기화 프로그램에서 람다가있는 병렬 스트림이 교착 상태를 일으키는 이유는 무엇입니까?

정적 이니셜 라이저에서 람다와 함께 병렬 스트림을 사용하는 데 CPU 사용률이없이 영원히 걸리는 이상한 상황을 발견했습니다. 코드는 다음과 같습니다.

class Deadlock {
    static {
        IntStream.range(0, 10000).parallel().map(i -> i).count();
        System.out.println("done");
    }
    public static void main(final String[] args) {}
}

이것은이 동작에 대한 최소한의 재현 테스트 케이스 인 것으로 보입니다. 만약 내가:

  • 정적 이니셜 라이저 대신 main 메서드에 블록을 넣습니다.
  • 병렬화 제거 또는
  • 람다를 제거하고

코드가 즉시 완료됩니다. 누구든지이 행동을 설명 할 수 있습니까? 버그입니까 아니면 의도 된 것입니까?

OpenJDK 버전 1.8.0_66-internal을 사용하고 있습니다.



답변

Stuart Marks에 의해 “Not an Issue”로 종료 된 매우 유사한 사례 ( JDK-8143380 ) 의 버그 보고서를 발견했습니다 .

이것은 클래스 초기화 교착 상태입니다. 테스트 프로그램의 메인 스레드는 클래스에 대한 초기화 진행 중 플래그를 설정하는 클래스 정적 이니셜 라이저를 실행합니다. 이 플래그는 정적 이니셜 라이저가 완료 될 때까지 설정된 상태로 유지됩니다. 정적 이니셜 라이저는 병렬 스트림을 실행하여 람다식이 다른 스레드에서 평가되도록합니다. 이러한 스레드는 클래스가 초기화를 완료하기를 기다리는 것을 차단합니다. 그러나 주 스레드는 병렬 작업이 완료되기를 기다리며 차단되어 교착 상태가됩니다.

클래스 정적 이니셜 라이저 외부로 병렬 스트림 논리를 이동하려면 테스트 프로그램을 변경해야합니다. 문제가 아닌 것으로 종결.


또 다른 버그 보고서 ( JDK-8136753 ) 를 찾을 수있었습니다 . 또한 Stuart Marks가 “Not an Issue”로 닫았습니다.

이것은 Fruit 열거 형의 정적 이니셜 라이저가 클래스 초기화와 잘못 상호 작용하기 때문에 발생하는 교착 상태입니다.

클래스 초기화에 대한 자세한 내용은 Java 언어 사양, 섹션 12.4.2를 참조하십시오.

http://docs.oracle.com/javase/specs/jls/se8/html/jls-12.html#jls-12.4.2

간단히 말해서, 무슨 일이 일어나고 있는지는 다음과 같습니다.

  1. 메인 스레드는 Fruit 클래스를 참조하고 초기화 프로세스를 시작합니다. 초기화 진행 중 플래그를 설정하고 기본 스레드에서 정적 초기화 프로그램을 실행합니다.
  2. 정적 이니셜 라이저는 다른 스레드에서 일부 코드를 실행하고 완료 될 때까지 기다립니다. 이 예제는 병렬 스트림을 사용하지만 스트림 자체와는 관련이 없습니다. 어떤 방법 으로든 다른 스레드에서 코드를 실행하고 해당 코드가 완료 될 때까지 기다리면 동일한 효과가 나타납니다.
  3. 다른 스레드의 코드는 초기화 진행 중 플래그를 확인하는 Fruit 클래스를 참조합니다. 이로 인해 플래그가 지워질 때까지 다른 스레드가 차단됩니다. (JLS 12.4.2의 2 단계를 참조하십시오.)
  4. 주 스레드는 다른 스레드가 종료 될 때까지 차단되므로 정적 초기화 프로그램이 완료되지 않습니다. 초기화 진행 중 플래그는 정적 이니셜 라이저가 완료 될 때까지 지워지지 않으므로 스레드는 교착 상태가됩니다.

이 문제를 방지하려면 다른 스레드가이 클래스가 초기화를 완료해야하는 코드를 실행하지 않도록하여 클래스의 정적 초기화를 빠르게 완료해야합니다.

문제가 아닌 것으로 종결.


참고 FindBugs은 경고를 추가 개방 문제가 이 상황을.


답변

Deadlock클래스 자체를 참조하는 다른 스레드가 어디에 있는지 궁금한 사람들을 위해 Java 람다는 다음과 같이 작동합니다.

public class Deadlock {
    public static int lambda1(int i) {
        return i;
    }
    static {
        IntStream.range(0, 10000).parallel().map(new IntUnaryOperator() {
            @Override
            public int applyAsInt(int operand) {
                return lambda1(operand);
            }
        }).count();
        System.out.println("done");
    }
    public static void main(final String[] args) {}
}

일반 익명 클래스에는 교착 상태가 없습니다.

public class Deadlock {
    static {
        IntStream.range(0, 10000).parallel().map(new IntUnaryOperator() {
            @Override
            public int applyAsInt(int operand) {
                return operand;
            }
        }).count();
        System.out.println("done");
    }
    public static void main(final String[] args) {}
}


답변

이 문제에 대한 훌륭한 설명이 2015 년 4 월 7 일자 Andrei Pangin 에 의해 작성되었습니다. 여기 에서 사용할 수 있지만 러시아어로 작성되어 있습니다 (어쨌든 코드 샘플을 검토하는 것이 좋습니다. 국제적입니다). 일반적인 문제는 클래스 초기화 중 잠금입니다.

다음은 기사의 인용문입니다.


JLS 에 따르면 모든 클래스에는 초기화 중에 캡처되는 고유 한 초기화 잠금 이 있습니다. 다른 스레드가 초기화 중에이 클래스에 액세스하려고하면 초기화가 완료 될 때까지 잠금에서 차단됩니다. 클래스가 동시에 초기화되면 교착 상태가 발생할 수 있습니다.

정수의 합을 계산하는 간단한 프로그램을 작성했는데 무엇을 인쇄해야합니까?

public class StreamSum {
    static final int SUM = IntStream.range(0, 100).parallel().reduce((n, m) -> n + m).getAsInt();

    public static void main(String[] args) {
        System.out.println(SUM);
    }
}

이제 제거 parallel() 람다를 하거나 Integer::sum호출로 바꾸십시오. 무엇이 변경됩니까?

여기서 교착 상태가 다시 나타납니다. [이 기사에서 이전에 클래스 이니셜 라이저에 교착 상태의 몇 가지 예가있었습니다]. parallel()스트림 작업은 별도의 스레드 풀에서 실행 되기 때문입니다 . 이 스레드는 바이트 코드로 작성된 람다 본문을 실행하려고합니다.private staticStreamSum 클래스 내부 메서드 . 그러나이 메서드는 스트림 완료 결과를 기다리는 클래스 정적 이니셜 라이저가 완료되기 전에는 실행할 수 없습니다.

더 놀라운 것은 무엇입니까?이 코드는 다른 환경에서 다르게 작동합니다. 단일 CPU 시스템에서 올바르게 작동하며 다중 CPU 시스템에서 중단 될 가능성이 높습니다. 이 차이는 Fork-Join 풀 구현에서 비롯됩니다. 매개 변수를 변경하여 직접 확인할 수 있습니다.-Djava.util.concurrent.ForkJoinPool.common.parallelism=N


답변