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


86

정적 이니셜 라이저에서 람다와 함께 병렬 스트림을 사용하는 데 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을 사용하고 있습니다.


4
범위 (0, 1)에서는 프로그램이 정상적으로 종료됩니다. (0, 2) 이상으로 중단됩니다.
Laszlo Hirdi


2
실제로 다른 API를 사용하면 정확히 동일한 질문 / 문제입니다.
Didier L

3
백그라운드 스레드에서 사용할 수 없도록 클래스 초기화를 완료하지 않은 경우 백그라운드 스레드에서 클래스를 사용하려고합니다.
Peter Lawrey

4
@ Solomonoff'sSecret 은 Deadlock 클래스에서 구현 된 i -> i메서드 참조가 아닙니다 static method. 교체 할 경우 i -> iFunction.identity()이 코드를 잘해야한다.
Peter Lawrey

답변:


71

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은 경고를 추가 개방 문제가 이 상황을.


20
"이는 기능을 설계 할 때 고려되었습니다.""이 버그의 원인을 알고 있지만 해결 방법 은 알 수 없습니다 . "는 "버그가 아님"을 의미 하지 않습니다 . 이것은 절대적으로 버그입니다.
BlueRaja-Danny Pflughoeft

13
@ bayou.io 주요 문제는 람다가 아닌 정적 초기화 프로그램 내에서 스레드를 사용하는 것입니다.
Stuart Marks

5
BTW Tunaki는 내 버그 보고서를 찾아 주셔서 감사합니다. :-)
Stuart Marks

13
@ bayou.io : 생성자에서와 마찬가지로 클래스 수준에서도 동일하며 this객체 생성 중에 탈출 할 수 있습니다. 기본 규칙은 이니셜 라이저에서 다중 스레드 작업을 사용하지 않는 것입니다. 나는 이것이 이해하기 어렵다고 생각하지 않는다. 람다 구현 함수를 레지스트리에 등록하는 예제는 다른 것입니다. 이러한 차단 된 백그라운드 스레드를 기다리지 않는 한 교착 상태를 만들지 않습니다. 그럼에도 불구하고 클래스 이니셜 라이저에서 이러한 작업을 수행하지 않는 것이 좋습니다. 그것이 의미하는 바가 아닙니다.
Holger

9
프로그래밍 스타일의 교훈은 다음과 같습니다. 정적 초기화를 단순하게 유지하십시오.
Raedwald

16

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

5
@ Solomonoff'sSecret 구현 선택입니다. 람다의 코드는 어딘가로 이동해야합니다. Javac는이를 포함하는 클래스의 정적 메소드로 컴파일합니다 ( lambda1이 예제와 유사 함 ). 각 람다를 자체 클래스에 넣는 것은 훨씬 더 비쌌을 것입니다.
Stuart Marks

1
@StuartMarks 람다가 기능적 인터페이스를 구현하는 클래스를 생성한다는 점을 감안할 때이 게시물의 두 번째 예제에서와 같이 기능적 인터페이스의 람다 구현에 람다 구현을 넣는 것이 효율적이지 않을까요? 그것은 확실히 일을하는 명백한 방법이지만 그들이있는 방식으로 일을하는 데에는 이유가 있다고 확신합니다.
모니카 복원

6
@ Solomonoff'sSecret 람다는 런타임시 ( java.lang.invoke.LambdaMetafactory 를 통해 ) 클래스를 생성 할 수 있지만 람다 본문은 컴파일 타임에 어딘가에 배치되어야합니다. 따라서 람다 클래스는 .class 파일에서로드 된 일반 클래스보다 저렴하기 위해 일부 VM 마법을 활용할 수 있습니다.
Jeffrey Bosboom

1
@ Solomonoff'sSecret 예, Jeffrey Bosboom의 답변이 정확합니다. 미래의 JVM에서 기존 클래스에 메소드를 추가 할 수있게되면 메타 팩토리는 새 클래스를 회전하는 대신이를 수행 할 수 있습니다. (순수 투기.)
스튜어트 마크

3
@Solomonoff의 비밀 : 당신과 같은 사소한 람다 표현을보고 판단하지 마십시오 i -> i. 그들은 표준이 아닙니다. Lambda 표현식은 private하나를 포함하여 주변 클래스의 모든 멤버를 사용할 수 있으며 정의 클래스 자체를 자연스러운 위치로 만듭니다. 이러한 모든 사용 사례가 클래스 이니셜 라이저의 특수한 경우에 최적화 된 구현으로 인해 정의 클래스의 멤버를 사용하지 않고 간단한 람다 식을 다중 스레드로 사용하는 것은 실행 가능한 옵션이 아닙니다.
Holger

14

이 문제에 대한 훌륭한 설명이 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

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.