[java] 루프에서 나머지 작업을 실행하는 Java 스레드는 다른 모든 스레드를 차단합니다.

다음 코드 조각은 두 개의 스레드를 실행합니다. 하나는 매초 간단한 타이머 로깅이고, 두 번째는 나머지 작업을 실행하는 무한 루프입니다.

public class TestBlockingThread {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestBlockingThread.class);

    public static final void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            int i = 0;
            while (true) {
                i++;
                if (i != 0) {
                    boolean b = 1 % i == 0;
                }
            }
        };

        new Thread(new LogTimer()).start();
        Thread.sleep(2000);
        new Thread(task).start();
    }

    public static class LogTimer implements Runnable {
        @Override
        public void run() {
            while (true) {
                long start = System.currentTimeMillis();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // do nothing
                }
                LOGGER.info("timeElapsed={}", System.currentTimeMillis() - start);
            }
        }
    }
}

결과는 다음과 같습니다.

[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1004
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1003
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=13331
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1006
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1003
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1004
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1004

무한 작업이 다른 모든 스레드를 13.3 초 동안 차단하는 이유를 이해할 수 없습니다. 스레드 우선 순위 및 기타 설정을 변경하려고 시도했지만 아무것도 작동하지 않았습니다.

이 문제를 해결하기위한 제안 (OS 컨텍스트 전환 설정 조정 포함)이 있으면 알려주세요.



답변

여기에서 모든 설명을 마친 후 ( Peter Lawrey 덕분에 )이 일시 중지의 주요 원인은 루프 내부의 safepoint에 거의 도달하지 않기 때문에 JIT 컴파일 된 코드 교체를 위해 모든 스레드를 중지하는 데 오랜 시간이 걸린다는 사실을 발견했습니다.

그러나 나는 더 깊이 들어가서 safepoint에 거의 도달하지 않는지 찾기로 결정 했습니다. while이 경우 루프 의 역 점프 가 “안전”하지 않은 이유가 약간 혼란 스러웠습니다 .

그래서 나는 -XX:+PrintAssembly모든 영광을 소환 하여

-XX:+UnlockDiagnosticVMOptions \
-XX:+TraceClassLoading \
-XX:+DebugNonSafepoints \
-XX:+PrintCompilation \
-XX:+PrintGCDetails \
-XX:+PrintStubCode \
-XX:+PrintAssembly \
-XX:PrintAssemblyOptions=-Mintel

몇 가지 조사 후 람다 C2컴파일러 의 세 번째 재 컴파일 후 루프 내부에서 safepoint 폴링이 완전히 사라 졌다는 것을 발견했습니다 .

최신 정보

프로파일 링 단계 변수 중 i 는 0과 같지 않은 것으로 나타났습니다 C2. 그래서이 분기를 추측 적으로 최적화하여 루프가 다음과 같은 것으로 변환되었습니다.

for (int i = OSR_value; i != 0; i++) {
    if (1 % i == 0) {
        uncommon_trap();
    }
}
uncommon_trap();

원래 무한 루프는 카운터가있는 일반 유한 루프로 재구성되었습니다! 유한 카운트 루프에서 safepoint 폴링을 제거하기위한 JIT 최적화로 인해이 루프에서도 safepoint 폴링이 없었습니다.

몇 시간 후, i 돌아가서 0흔하지 않은 함정을 가져갔습니다. 이 메서드는 최적화되지 않았고 인터프리터에서 계속 실행되었습니다. 새로운 지식 C2으로 재 컴파일하는 동안 무한 루프를 인식하고 컴파일을 포기했습니다. 나머지 방법은 적절한 safepoint를 사용하여 인터프리터에서 진행되었습니다.

반드시 읽어야 할 훌륭한 블로그 게시물 “Safepoints : 의미, 부작용 및 오버 헤드”가 있습니다.Safepoint와이 특정 문제를 다루는 Nitsan Wakart 가 있습니다.

매우 긴 카운트 루프에서 Safepoint 제거는 문제로 알려져 있습니다. 버그 JDK-5014723(감사합니다 Vladimir Ivanov )는이 문제를 해결합니다.

버그가 최종적으로 수정 될 때까지 해결 방법을 사용할 수 있습니다.

  1. 사용을 시도 할 수 있습니다 -XX:+UseCountedLoopSafepoints( 전체 성능 저하 유발 하고 JVM 충돌로 이어질 수 있습니다 JDK-8161147 ). 사용 후C2 컴파일러는 뒤로 점프에서 safepoints를 계속 유지하고 원래 일시 중지는 완전히 사라집니다.
  2. 다음을 사용하여 문제가있는 메서드의 컴파일을 명시 적으로 비활성화 할 수 있습니다.
    -XX:CompileCommand='exclude,binary/class/Name,methodName'

  3. 또는 수동으로 safepoint를 추가하여 코드를 다시 작성할 수 있습니다. 예를 들어 Thread.yield()주기의 끝에서 호출 또는 변경 int ilong i(덕분에, Nitsan Wakart는 )도 일시 정지가 해결됩니다.


답변

요컨대, 당신이 가지고있는 루프는 i == 0 도달 는 . 이 메소드가 컴파일되고 교체 될 코드를 트리거 할 때 모든 스레드를 안전한 지점으로 가져와야하지만 이는 코드를 실행하는 스레드뿐만 아니라 JVM의 모든 스레드를 잠그는 데 매우 오랜 시간이 걸립니다.

다음 명령 줄 옵션을 추가했습니다.

-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime -XX:+PrintCompilation

또한 시간이 오래 걸리는 부동 소수점을 사용하도록 코드를 수정했습니다.

boolean b = 1.0 / i == 0;

그리고 출력에서 ​​보는 것은

timeElapsed=100
Application time: 0.9560686 seconds
  41423  280 %     4       TestBlockingThread::lambda$main$0 @ -2 (27 bytes)   made not entrant
Total time for which application threads were stopped: 40.3971116 seconds, Stopping threads took: 40.3967755 seconds
Application time: 0.0000219 seconds
Total time for which application threads were stopped: 0.0005840 seconds, Stopping threads took: 0.0000383 seconds
  41424  281 %     3       TestBlockingThread::lambda$main$0 @ 2 (27 bytes)
timeElapsed=40473
  41425  282 %     4       TestBlockingThread::lambda$main$0 @ 2 (27 bytes)
  41426  281 %     3       TestBlockingThread::lambda$main$0 @ -2 (27 bytes)   made not entrant
timeElapsed=100

참고 : 코드를 교체하려면 안전한 지점에서 스레드를 중지해야합니다. 그러나 여기에서는 그러한 안전한 지점에 매우 드물게 도달하는 것으로 보입니다 ( i == 0작업을 다음으로 변경하는 경우에만 가능).

Runnable task = () -> {
    for (int i = 1; i != 0 ; i++) {
        boolean b = 1.0 / i == 0;
    }
};

비슷한 지연이 있습니다.

timeElapsed=100
Application time: 0.9587419 seconds
  39044  280 %     4       TestBlockingThread::lambda$main$0 @ -2 (28 bytes)   made not entrant
Total time for which application threads were stopped: 38.0227039 seconds, Stopping threads took: 38.0225761 seconds
Application time: 0.0000087 seconds
Total time for which application threads were stopped: 0.0003102 seconds, Stopping threads took: 0.0000105 seconds
timeElapsed=38100
timeElapsed=100

루프에 코드를 신중하게 추가하면 지연이 더 길어집니다.

for (int i = 1; i != 0 ; i++) {
    boolean b = 1.0 / i / i == 0;
}

얻다

 Total time for which application threads were stopped: 59.6034546 seconds, Stopping threads took: 59.6030773 seconds

그러나 항상 안전한 지점이있는 네이티브 메서드를 사용하도록 코드를 변경하십시오 (내재적이지 않은 경우).

for (int i = 1; i != 0 ; i++) {
    boolean b = Math.cos(1.0 / i) == 0;
}

인쇄물

Total time for which application threads were stopped: 0.0001444 seconds, Stopping threads took: 0.0000615 seconds

참고 : if (Thread.currentThread().isInterrupted()) { ... }루프에 추가하면 안전한 지점이 추가됩니다.

참고 : 이는 16 코어 머신에서 발생하므로 CPU 리소스가 부족하지 않습니다.


답변

이유에 대한 답을 찾았습니다 . 그들은 safepoints라고 불리며 GC로 인해 발생하는 Stop-The-World로 가장 잘 알려져 있습니다.

이 기사를 참조하십시오 : JVM에서 stop-the-world 일시 중지 로깅

다른 이벤트로 인해 JVM이 모든 애플리케이션 스레드를 일시 중지 할 수 있습니다. 이러한 일시 중지를 STW (Stop-The-World) 일시 중지라고합니다. STW 일시 중지가 트리거되는 가장 일반적인 원인은 가비지 수집 (github의 예)이지만 다른 JIT 작업입니다. (예), 편향된 잠금 취소 (예), 특정 JVMTI 작업 등도 애플리케이션을 중지해야합니다.

애플리케이션 스레드가 안전하게 중지 될 수있는 지점을 놀라움, safepoints 라고 합니다. 이 용어는 또한 모든 STW 일시 중지를 나타내는 데 자주 사용됩니다.

GC 로그를 사용하는 것은 다소 일반적입니다. 그러나 이것은 모든 Safepoint에 대한 정보를 캡처하지는 않습니다. 모든 것을 얻으려면 다음 JVM 옵션을 사용하십시오.

-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime

GC를 명시 적으로 참조하는 이름 지정에 대해 궁금한 경우 놀라지 마십시오.이 옵션을 켜면 가비지 수집 일시 중지뿐만 아니라 모든 safepoint가 기록됩니다. 위에 지정된 플래그를 사용하여 다음 예제 (github의 소스)를 실행하는 경우.

HotSpot 용어집을 읽고 다음을 정의합니다.

Safepoint

프로그램 실행 중 모든 GC 루트가 알려져 있고 모든 힙 개체 내용이 일치하는 지점입니다. 글로벌 관점에서 모든 스레드는 GC가 실행되기 전에 안전한 지점에서 차단되어야합니다. (특별한 경우로 JNI 코드를 실행하는 스레드는 핸들 만 사용하기 때문에 계속 실행될 수 있습니다. safepoint 중에는 핸들의 내용을로드하는 대신 차단해야합니다.) 로컬 관점에서 Safepoint는 구별 지점입니다. 실행 스레드가 GC에 대해 차단 될 수있는 코드 블록에서.대부분의 통화 사이트는 Safepoint로 인정됩니다.안전하지 않은 지점에서는 무시 될 수있는 모든 안전 지점에서 적용되는 강력한 불변성이 있습니다. 컴파일 된 Java 코드와 C / C ++ 코드는 모두 safepoint 사이에서 최적화되지만 safepoint에서는 덜 최적화됩니다. JIT 컴파일러는 각 safepoint에서 GC 맵을 내 보냅니다. VM의 C / C ++ 코드는 양식화 된 매크로 기반 규칙 (예 : TRAPS)을 사용하여 잠재적 인 Safepoint를 표시합니다.

위에서 언급 한 플래그로 실행하면 다음과 같은 출력이 나타납니다.

Application time: 0.9668750 seconds
Total time for which application threads were stopped: 0.0000747 seconds, Stopping threads took: 0.0000291 seconds
timeElapsed=1015
Application time: 1.0148568 seconds
Total time for which application threads were stopped: 0.0000556 seconds, Stopping threads took: 0.0000168 seconds
timeElapsed=1015
timeElapsed=1014
Application time: 2.0453971 seconds
Total time for which application threads were stopped: 10.7951187 seconds, Stopping threads took: 10.7950774 seconds
timeElapsed=11732
Application time: 1.0149263 seconds
Total time for which application threads were stopped: 0.0000644 seconds, Stopping threads took: 0.0000368 seconds
timeElapsed=1015

세 번째 STW 이벤트를 확인하십시오.
중지 된 총 시간 : 10.7951187 초
스레드 중지 시간 : 10.7950774 초

JIT 자체는 사실상 시간이 걸리지 않았지만 JVM이 JIT 컴파일을 수행하기로 결정한 후 STW 모드에 들어 갔지만 컴파일 할 코드 (무한 루프)에 호출 사이트 가 없으므로 Safepoint에 도달 하지 못했습니다 .

STW는 JIT가 결국 대기를 포기하고 코드가 무한 루프에 있다고 결론을 내릴 때 종료됩니다.


답변

주석 스레드와 일부 테스트를 직접 수행 한 후 일시 중지가 JIT 컴파일러에 의해 발생했다고 생각합니다. JIT 컴파일러가 그렇게 오래 걸리는 이유는 디버깅 능력을 넘어선 것입니다.

그러나 이것을 방지하는 방법 만 요청했기 때문에 해결책이 있습니다.

무한 루프를 JIT 컴파일러에서 제외 할 수있는 메서드로 가져옵니다.

public class TestBlockingThread {
    private static final Logger LOGGER = Logger.getLogger(TestBlockingThread.class.getName());

    public static final void main(String[] args) throws InterruptedException     {
        Runnable task = () -> {
            infLoop();
        };
        new Thread(new LogTimer()).start();
        Thread.sleep(2000);
        new Thread(task).start();
    }

    private static void infLoop()
    {
        int i = 0;
        while (true) {
            i++;
            if (i != 0) {
                boolean b = 1 % i == 0;
            }
        }
    }

다음 VM 인수를 사용하여 프로그램을 실행합니다.

-XX : CompileCommand = exclude, PACKAGE.TestBlockingThread :: infLoop (PACKAGE를 패키지 정보로 대체)

메소드가 JIT 컴파일되었을 때를 나타내는 다음과 같은 메시지가 표시되어야합니다.
### Excluding compile : static blocking.TestBlockingThread :: infLoop
클래스를 blocking이라는 패키지에 넣었 음을
알 수 있습니다.


답변