code

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

codestyles 2020. 10. 8. 08:02
반응형

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


정적 이니셜 라이저에서 람다와 함께 병렬 스트림을 사용하는 데 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 에 의해 작성되었습니다. 여기 에서 사용할 수 있지만 러시아어로 작성되었습니다 (어쨌든 코드 샘플을 검토하는 것이 좋습니다. 국제적입니다). 일반적인 문제는 클래스 초기화 중 잠금입니다.

다음은 기사에서 인용 한 내용입니다.


According to JLS, every class has a unique initialization lock that is captured during initialization. When other thread tries to access this class during initialization, it will be blocked on the lock until initialization completes. When classes are initialized concurrently, it is possible to get a deadlock.

I wrote a simple program that calculates the sum of integers, what should it print?

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);
    }
} 

Now remove parallel() or replace lambda with Integer::sum call - what will change?

Here we see deadlock again [there were some examples of deadlocks in class initializers previously in the article]. Because of the parallel() stream operations run in a separate thread pool. These threads try to execute lambda body, which is written in bytecode as a private static method inside StreamSum class. But this method can not be executed before the completion of class static initializer, which waits the results of stream completion.

What is more mindblowing: this code works differently in different environments. It will work correctly on a single CPU machine and will most likely hang on a multi CPU machine. This difference comes from the Fork-Join pool implementation. You can verify it yourself changing the parameter -Djava.util.concurrent.ForkJoinPool.common.parallelism=N

참고URL : https://stackoverflow.com/questions/34820066/why-does-parallel-stream-with-lambda-in-static-initializer-cause-a-deadlock

반응형