1. 개요
이 기사에서는 Java 스레드 상태 , 특히 Thread.State.WAITING 을 살펴보겠습니다 . 스레드가 이 상태에 들어가는 방법과 그 차이점을 살펴보겠습니다. 마지막으로 동기화를 위한 여러 정적 유틸리티 메서드를 제공 하는 LockSupport 클래스 를 자세히 살펴보겠습니다 .
2. Thread.State.WAITING 진입
Java는 스레드를 WAITING 상태로 만드는 여러 방법을 제공합니다.
2.1. Object.wait()
스레드를 WAITING 상태로 만드는 가장 표준적인 방법 중 하나는 wait() 메서드 를 사용하는 것 입니다. 스레드 가 객체의 모니터를 소유하면 다른 스레드가 일부 작업을 완료하고 notify() 메서드 를 사용하여 깨울 때까지 실행을 일시 중지할 수 있습니다 . 실행이 일시 중지된 동안 스레드는 WAITING(객체 모니터에서) 상태이며 프로그램의 스레드 덤프 에도 보고됩니다 .
"WAITING-THREAD" #11 prio=5 os_prio=0 tid=0x000000001d6ff800 nid=0x544 in Object.wait() [0x000000001de4f000]
java.lang.Thread.State: WAITING (on object monitor)
2.2. Thread.join()
스레드 실행을 일시 중지하는 데 사용할 수 있는 또 다른 방법은 join() 호출 을 사용하는 것 입니다. 메인 스레드가 작업자 스레드가 먼저 완료될 때까지 기다려야 할 때 메인 스레드 에서 작업자 스레드 인스턴스에 대해 join() 을 호출할 수 있습니다 . 실행이 일시 중지되고 메인 스레드가 WAITING 상태로 들어가고 jstack 에서 WAITING(객체 모니터에서) 으로 보고됩니다 .
"MAIN-THREAD" #12 prio=5 os_prio=0 tid=0x000000001da4f000 nid=0x25f4 in Object.wait() [0x000000001e28e000]
java.lang.Thread.State: WAITING (on object monitor)
2.3. LockSupport.park()
마지막으로 LockSupport 클래스 의 park() 정적 메서드를 사용하여 스레드를 WAITING 상태로 설정할 수도 있습니다 . park() 를 호출 하면 현재 스레드에 대한 실행이 중지되고 WAITING 상태가 됩니다. 더 구체적으로 말하면 jstack 보고서에 표시 되는 WAITING(주차) 상태 입니다.
"PARKED-THREAD" #11 prio=5 os_prio=0 tid=0x000000001e226800 nid=0x43cc waiting on condition [0x000000001e95f000]
java.lang.Thread.State: WAITING (parking)
스레드 파킹과 언파킹을 더 잘 이해하고 싶기 때문에 이것이 어떻게 작동하는지 자세히 살펴보겠습니다.
3. 주차 및 주차 해제 스레드
이전에 보았듯이 LockSupport 클래스에서 제공하는 기능을 사용하여 스레드를 파킹 및 파킹 해제할 수 있습니다. 이 클래스는 Unsafe 클래스 의 래퍼 이며 대부분의 메서드가 즉시 이 클래스에 위임됩니다. 그러나 Unsafe 는 내부 Java API로 간주되어 사용해서는 안 되므로 LockSupport 는 주차 유틸리티에 액세스할 수 있는 공식적인 방법 입니다.
3.1. LockSupport 사용 방법
LockSupport 를 사용 하는 것은 간단합니다. 스레드의 실행을 중지하려면 park() 메서드를 호출합니다. 스레드 개체 자체에 대한 참조를 제공할 필요가 없습니다. 코드는 이를 호출하는 스레드를 중지합니다.
간단한 주차 예를 살펴보겠습니다.
public class Application {
public static void main(String[] args) {
Thread t = new Thread(() -> {
int acc = 0;
for (int i = 1; i <= 100; i++) {
acc += i;
}
System.out.println("Work finished");
LockSupport.park();
System.out.println(acc);
});
t.setName("PARK-THREAD");
t.start();
}
}
1에서 100까지의 숫자를 누적하여 출력하는 최소한의 콘솔 애플리케이션을 만들었습니다. 실행하면 Work Finished 가 인쇄 되지만 결과는 인쇄되지 않는 것을 볼 수 있습니다. 물론 바로 전에 park() 를 호출하기 때문입니다.
PARK-THREAD 가 완료 되도록 하려면 언파크해야 합니다 . 이렇게 하려면 다른 스레드를 사용해야 합니다. 메인 스레드( main() 메서드를 실행하는 스레드)를 사용 하거나 새 스레드를 만들 수 있습니다.
간단하게 메인 스레드를 사용하겠습니다.
t.setName("PARK-THREAD");
t.start();
Thread.sleep(1000);
LockSupport.unpark(t);
우리는 PARK-THREAD 가 축적을 끝내고 스스로 파킹할 수 있도록 메인 스레드 에 1초의 휴면을 추가 합니다. 그런 다음 unpark(Thread) 를 호출하여 언파크합니다 . 예상대로 주차를 해제하는 동안 시작하려는 스레드 개체에 대한 참조를 제공해야 합니다.
변경 사항으로 프로그램은 이제 제대로 종료되고 결과 5050 을 인쇄합니다 .
3.2. 언파크 허가
주차 API의 내부는 허가증을 사용하여 작동합니다. 이것은 실제로 단일 허가 Semaphore 처럼 작동합니다 . 공원 허가는 그것을 소비하는 park () 메소드 로 스레드의 상태를 관리하기 위해 내부적으로 사용되는 반면, unpark() 는 이를 사용 가능하게 합니다.
스레드당 하나의 허가만 사용할 수 있으므로 unpark() 메서드를 여러 번 호출해도 효과가 없습니다. 단일 park() 호출은 스레드를 비활성화합니다.
그러나 흥미로운 점은 파킹된 스레드가 다시 활성화되기 위해 허가가 사용 가능해질 때까지 대기한다는 것입니다. park() 를 호출할 때 이미 허가가 있는 경우 스레드가 비활성화되지 않습니다. 허가가 소모되고 park() 호출이 즉시 반환되고 스레드가 계속 실행됩니다.
이전 코드 스니펫에서 sleep() 호출을 제거하여 이 효과를 볼 수 있습니다 .
//Thread.sleep(1000);
LockSupport.unpark(t);
프로그램을 다시 실행하면 PARK-THREAD 실행에 지연이 없음을 알 수 있습니다. 이는 unpark() 를 즉시 호출하여 park() 에 대한 허가를 제공하기 때문입니다 .
3.3. 주차 과부하
LockSupport 클래스에는 park(Object blocker) 오버로드 메서드 가 포함되어 있습니다 . blocker 인수 는 스레드 파킹을 담당하는 동기화 개체입니다. 우리가 제공하는 개체는 해당 파킹 프로세스에 영향을 미치지 않지만 스레드 덤프에 보고되므로 동시성 문제를 진단하는 데 도움이 될 수 있습니다.
동기화 개체를 포함하도록 코드를 변경해 보겠습니다.
Object syncObj = new Object();
LockSupport.park(syncObj);
unpark() 호출을 제거하고 응용 프로그램을 다시 실행하면 중단됩니다. jstack 을 사용 하여 PARK-THREAD 가 수행하는 작업을 확인하면 다음을 얻을 수 있습니다.
"PARK-THREAD" #11 prio=5 os_prio=0 tid=0x000000001e401000 nid=0xfb0 waiting on condition [0x000000001eb4f000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x000000076b4a8690> (a java.lang.Object)
보시다시피, 마지막 줄에는 PARK-THREAD 가 기다리고 있는 개체가 포함되어 있습니다. 이것은 디버깅 목적에 유용하므로 park(Object) 오버로드를 선호해야 합니다.
4. 주차 vs. 대기
이 두 API 모두 유사한 기능을 제공하므로 어떤 것을 선호해야 합니까? 일반적으로 LockSupport 클래스와 해당 기능은 하위 수준 API로 간주됩니다. 또한 API는 쉽게 오용되어 모호한 교착 상태가 발생할 수 있습니다. 대부분의 경우 Thread 클래스의 wait() 및 join() 을 사용해야 합니다 .
파킹을 사용하는 이점은 스레드를 비활성화하기 위해 동기화된 블록을 입력할 필요가 없다는 것 입니다. 이것은 동기화된 블록 이 코드에서 발생 이전 관계 를 설정하기 때문에 중요합니다. 이는 모든 변수를 강제로 새로 고치고 필요하지 않은 경우 잠재적으로 성능을 저하시킬 수 있습니다. 그러나 이 최적화는 거의 실행되지 않아야 합니다.
5. 결론
이 기사에서는 LockSupport 클래스와 해당 파킹 API를 살펴보았습니다. 우리는 그것을 사용하여 스레드를 비활성화하는 방법을 살펴보고 내부적으로 어떻게 작동하는지 설명했습니다. 마지막으로 더 일반적인 wait()/join() API와 비교하고 차이점을 보여주었습니다.
항상 그렇듯이 코드 예제는 GitHub 에서 찾을 수 있습니다 .