역사적으로 Java는 선제적이고 제한된 인터럽트를 제공하려고 노력했지만 앞서 소개한 Thread.stop, Thread.suspension 및 Thread.resume이 포기되는 등 많은 문제가 있었습니다. 반면, Java 애플리케이션 코드의 견고성을 고려하여 프로그래밍 임계값이 낮아지고 기본 메커니즘을 모르는 프로그래머가 의도치 않게 시스템을 손상시킬 가능성이 줄어듭니다.
현재 Java의 스레드 스케줄링은 선점형 인터럽트를 제공하지 않지만 협력형 인터럽트를 사용합니다. 실제로 협력적 중단의 원리는 매우 간단합니다. 즉, 중단을 나타내는 특정 표시를 폴링하는 것입니다. 이는 모든 일반 코드에서 구현할 수 있습니다.
예를 들어 다음 코드는 다음과 같습니다.
휘발성 bool isInterrupted;
//…
동안(!isInterrupted) {
계산();
}
그러나 위의 코드 문제도 분명합니다. 계산 실행 시간이 상대적으로 길면 인터럽트에 제때 응답할 수 없습니다. 반면 폴링을 사용하여 플래그 변수를 확인하면 대기 및 절전과 같은 스레드 차단 작업을 중단할 방법이 없습니다.
위의 아이디어를 계속 사용한다면 인터럽트가 적시에 응답하도록 하려면 가상 머신 하단의 스레드 스케줄링을 통해 mark 변수를 확인해야 합니다. 예, 이는 실제로 JVM에서 수행됩니다.
다음은 java.lang.Thread의 소스 코드에서 발췌한 것입니다.
공개 정적 부울 중단됨() {
currentThread().isInterrupted(true)를 반환합니다.
}
//…
개인 네이티브 부울 isInterrupted(boolean ClearInterrupted);
isInterrupted가 JVM의 기본 구현에 따라 달라지는 기본 메소드로 선언되었음을 알 수 있습니다.
실제로 JVM은 각 스레드에 대해 내부적으로 인터럽트 플래그를 유지합니다. 그러나 애플리케이션은 이 인터럽트 변수에 직접 접근할 수 없으며 다음 방법을 통해 작동해야 합니다.
공개 클래스 스레드 {
//인터럽트 마크 설정
공개 무효 인터럽트() { ... }
//인터럽트 표시 값을 가져옵니다.
공개 부울 isInterrupted() { ... }
//인터럽트 표시를 지우고 마지막 인터럽트 표시의 값을 반환합니다.
공개 정적 부울 중단됨() { ... }
}
일반적으로 스레드의 인터럽트 메소드를 호출하면 즉시 인터럽트가 발생하지 않고 JVM 내부에 인터럽트 플래그만 설정됩니다. 따라서 인터럽트 플래그를 확인함으로써 애플리케이션은 특별한 작업을 수행하거나 인터럽트를 완전히 무시할 수 있습니다.
JVM이 이러한 조잡한 인터럽트 메커니즘만 제공한다면 기본적으로 인터럽트 변수를 정의하고 폴링하는 애플리케이션 자체의 방법에 비해 아무런 이점이 없다고 생각할 수도 있습니다.
JVM 내부 인터럽트 변수의 주요 장점은 특정 상황에 대해 자동 "인터럽트 트랩"을 시뮬레이션하는 메커니즘을 제공한다는 것입니다.
스레드 스케줄링(예: 대기, 휴면 및 조인)과 관련된 차단 호출을 실행할 때 중단이 발생하면 차단된 스레드는 "가능한 한 빨리" InterruptedException을 발생시킵니다. 따라서 다음 코드 프레임워크를 사용하여 스레드 차단 인터럽트를 처리할 수 있습니다.
노력하다 {
//대기, 잠자기 또는 참여
}
catch(InterruptedException e) {
//일부 인터럽트 처리 작업
}
"가능한 한 빨리"라는 것은 JVM이 스레드 스케줄링 사이의 간격에서 인터럽트 변수를 확인하는 것 같습니다. 속도는 JVM의 구현과 하드웨어 성능에 따라 다릅니다.
그러나 특정 스레드 차단 작업의 경우 JVM은 InterruptedException을 자동으로 발생시키지 않습니다. 예를 들어 특정 I/O 작업 및 내부 잠금 작업이 있습니다. 이러한 유형의 작업에서는 인터럽트를 다른 방법으로 시뮬레이션할 수 있습니다.
1) java.io의 비동기 소켓 I/O
소켓을 읽고 쓸 때, InputStream 및 OutputStream의 읽기 및 쓰기 메소드는 차단 및 대기하지만 Java 인터럽트에는 응답하지 않습니다. 그러나 Socket의 닫기 메서드를 호출한 후 차단된 스레드는 SocketException을 발생시킵니다.
2) Selector를 사용하여 비동기 I/O 구현
스레드가 Selector.select(java.nio.channels)에서 차단된 경우 wakeup 메소드를 호출하면 ClosedSelectorException 예외가 발생합니다.
3) 잠금 획득
스레드가 내부 잠금을 획득하기 위해 대기 중인 경우 이를 중단할 수 없습니다. 그러나 Lock 클래스의 lockInterruptible 메소드를 사용하면 잠금을 기다리는 동안 인터럽트 기능을 제공할 수 있습니다.
또한 작업과 스레드가 분리된 프레임워크에서 작업은 일반적으로 어떤 스레드에 의해 호출될지 알 수 없으므로 호출 스레드의 중단 처리 전략을 알 수 없습니다. 따라서 작업이 스레드 중단 플래그를 설정한 후에는 작업이 취소된다는 보장이 없습니다. 따라서 프로그래밍 원칙에는 두 가지가 있습니다.
1) 인터럽트 정책을 알지 않는 한 스레드를 인터럽트해서는 안됩니다.
이 원칙은 Executer와 같은 프레임워크에서 스레드의 인터럽트 메서드를 직접 호출해서는 안 되며, 작업을 취소하려면 Future.cancel과 같은 메서드를 사용해야 함을 알려줍니다.
2) 태스크 코드는 인터럽트가 실행 스레드에 어떤 의미인지 추측해서는 안 됩니다.
이 원칙은 일반 코드에서 InterruptedException이 발생하면 이를 잡아서 "삼키지" 말고 상위 코드에 계속해서 던져야 함을 알려줍니다.
간단히 말해서, Java의 비선점형 인터럽트 메커니즘을 사용하려면 기존의 선점형 인터럽트 개념을 바꾸고 본질에 대한 이해를 바탕으로 프로그래밍에 대한 해당 원칙과 패턴을 채택해야 합니다.