질문의 역사
Java 5 이전에는 스레드 조정이 Thread.suspend(고유한 교착 상태 위험으로 인해 사용 중지됨) 또는 Object.wait/notify와 같은 원시 메서드에 의존했습니다. 이것은 엄격한 모니터 소유권이 필요하며 대기 전에 알림이 발생할 경우 깨어남 손실 문제가 발생했습니다. Java 5에서 java.util.concurrent의 도입으로 LockSupport는 AbstractQueuedSynchronizer와 같은 고성능 동기화기를 구축할 수 있도록 설계된 저수준의 언블로킹 원시입니다.
문제
동시 프로그래밍에서 전형적인 경쟁 조건은 신호 스레드가 대상 스레드가 실제로 주차하기 전에 unpark 메커니즘을 호출할 때 발생합니다. 전통적인 조건 변수에서는 이 신호가 손실되어 대상 스레드가 무한히 대기하게 됩니다. 순진한 해결책은 세미포어 카운트를 사용하여 허가를 누적하는 것일 수 있지만, 이것은 불필요한 복잡성과 소비자가 생산자를 초과할 경우 리소스 누수의 잠재력을 도입합니다.
해결책
LockSupport는 각 스레드와 관련된 비축적 단일 비트 허가를 사용합니다. 이 허가는 일회용 스레드 로컬 게이트 패스 역할을 합니다:
허가는 누적되지 않기 때문에(1에서 포화됨), 이는 과도한 unpark로 인한 메모리 누수를 방지하며, 주차 이전에 발행된 unpark가 기억되도록 보장하므로, 발생 전 관계를 통해 깨어남 손실 문제를 제거합니다.
import java.util.concurrent.locks.LockSupport; public class PermitExample { public static void main(String[] args) throws InterruptedException { Thread worker = new Thread(() -> { System.out.println("작업자: 초기 작업..."); try { Thread.sleep(100); } catch (InterruptedException e) {} System.out.println("작업자: 주차 시도..."); LockSupport.park(); System.out.println("작업자: 성공적으로 언파크됨!"); }); worker.start(); // 작업자가 실제로 주차되기 전에 신호 보내기 Thread.sleep(50); System.out.println("주요: 작업자가 주차되기 전에 언파크 호출"); LockSupport.unpark(worker); worker.join(); } }
문제 설명
고주파 거래 시스템의 주문 일치 엔진을 설계하면서, 생산자가 큐 상태를 확인할 수 있도록 잠금을 유지하지 않고도 수신 대기열이 용량에 도달했을 때 소비자 스레드가 처리를 멈출 수 있는 역압 메커니즘이 필요했습니다. 표준 ReentrantLock과 Condition은 신호 전송 중 큐의 잠금에 대해 경쟁이 발생했습니다. Object.wait/notify는 높은 소용돌이 경쟁 중 깨어남 손실의 위험이 있었습니다.
고려된 다른 해결책들
1. Object.wait/notifyAll
이 접근 방식은 큐의 고유 잠금을 사용했습니다. 장점: 표준 모니터를 사용하여 구현하기 쉬움. 단점: 신호를 보내기 위해 생산자가 모니터를 얻어야 하므로 직렬화 병목 현상이 발생함. 더 나쁘게도, 생산자가 소비자가 큐 크기를 확인하고 대기 호출 사이의 짧은 윈도우에서 notify를 호출하면 신호가 손실되어 소비자가 영구적으로 교착 상태에 빠질 수 있었습니다.
2. 여러 조건이 있는 ReentrantLock
"가득 찼음"과 "비어있음" 상태에 대해 별도의 조건을 사용하려고 했습니다. 장점: 고유 잠금보다 더 유연하여 선택적인 깨어남을 허용함. 단점: 여전히 신호 전송에 잠금을 획득해야 했으며(신호 전송), 조건 큐 간 스레드를 올바르게 전송하는 복잡성이 유지 관리 오버헤드를 증가시켜 기본 잠금 오버헤드를 해결하지 못했습니다.
3. 명시적 원자 상태가 있는 LockSupport
선택된 해결책은 진행 허가를 나타내기 위해 AtomicBoolean을 사용하고 차단을 위해 LockSupport를 사용하는 것이었습니다. 큐가 가득 차면 소비자는 원자적으로 "needsParking" 플래그를 설정한 다음 주차했습니다. 생산자는 항목을 제거한 후 플래그를 확인하고 설정되어 있으면 unpark를 호출했습니다. 장점: 신호 전송에는 잠금이 필요하지 않으며, 깨어남 동안의 경쟁을 제거했습니다. 단일 비트 허가 모델은 생산자가 소비자가 주차를 호출하기 직전에 unpark를 호출하더라도(CPU 스케줄링 덕분에) 깨어남이 손실되지 않도록 보장했습니다.
선택된 해결책과 결과
우리는 LockSupport 접근 방식을 선택했습니다. 신호 메커니즘을 큐의 구조적 잠금에서 분리함으로써 우리는 높은 부하 하에서 생산자 지연을 40% 줄이고 스트레스 테스트 중 관찰된 깨어남 손실 시나리오를 제거했습니다. 명시적인 상태 관리를 통해(unpark 후 조건을 재확인) park()의 자발적 깨어남 계약에도 불구하고 정확성을 보장했습니다.
LockSupport.park가 스레드가 보유한 모니터의 소유권을 해제합니까?
아니요. 이것은 **Object.wait()**와의 중요한 차이점입니다. 스레드가 LockSupport.park를 호출하면 기다리는 상태에 들어가지만 현재 보유한 모든 모니터의 소유권을 유지합니다. 다른 스레드가 해당 모니터 중 하나에 들어가려고 하면(예: 동일한 객체에 대한 synchronized 블록), 차단되며, 주차된 스레드만 해제할 수 있는 경우 교착 상태가 발생할 수 있습니다. 후보자들은 종종 park가 wait와 같다고 잘못 생각하고 잠금을 해제한다고 가정합니다. 이는 순수하게 스레드-로컬 스케줄러 원시입니다.
interrupt 상태가 설정된 스레드에서 LockSupport.park를 호출할 때의 동작은 무엇입니까?
이 메서드는 즉시 반환되며 차단되지 않으며 interrupt 상태를 해제하지도 않습니다. 이것은 인터럽트를 상태를 해제하고 InterruptedException을 발생시키는 **Object.wait()**와 근본적으로 다릅니다. LockSupport에서는 스레드가 인터럽트 관례를 준수하고자 하는 경우(완전히 확인하고 인터럽트 상태를 제거해야 함) **Thread.interrupted()**를 통해 명시적으로 확인해야 합니다. 이 설계는 park가 비인터럽트 가능한 맥락에서 사용되거나 인터럽트가 주차 허가와 분리된 문제로 처리될 수 있도록 해줍니다.
LockSupport는 어떻게 자발적 깨어남을 처리하며, 이것이 코딩 패턴에 어떤 영향을 미칩니까?
LockSupport.park는 "어떠한 이유 없이"(자발적 깨어남) 반환한다고 문서화되어 있지만, 실제로는 현대 JVM에서 드뭅니다. 허가 기반 깨어남(unpark)과는 달리, 자발적 깨어남은 허가를 소비하지 않습니다. 그렇기 때문에 호출자는 항상 루프에서 주차의 원인이 된 조건을 재확인해야 합니다:
while (!canProceed()) { LockSupport.park(); }
후보자들은 종종 park 후 단순히 조건을 한 번 확인하는 것으로는 충분하지 않다고 간과합니다. 스레드는 자발적 깨어남(또는 우연히 발생한 인터럽트)으로 인해 unpark 호출 없이 깨어날 수 있으므로 상태 조건의 재평가가 필요합니다. 허가는 유효한 unpark가 손실되지 않도록 보장하지만, 자발적 반환을 방지하지는 않습니다.