ScheduledThreadPoolExecutor는 Java 5에서 도입된 강력한 스레드 안전 구현으로, uncaught exception이 발생할 경우 치명적인 단일 스레드 종료 문제를 겪는 java.util.Timer의 대체제로 등장했습니다. 시간적 이상은 내부 ScheduledFutureTask 구현으로부터 발생하며, 여기서 주기는 long으로 저장되어 양수 값은 고정 속도 의미론(절대 시간 스케줄링)을 나타내고, 음수 값은 고정 지연 의미론(상대 시간 스케줄링)을 나타냅니다. 주기적 작업의 실행 기간이 간격을 초과하면, 고정 속도 방식은 작업을 쉬지 않고 연속적으로 실행하여 스케줄을 유지하려고 시도하고 이는 드리프트와 잠재적인 자원 소모를 초래하며, 고정 지연 방식은 각 완료 후 필수적인 휴식을 삽입하여 시스템 안정성을 확보하기 위해 시간적 변화를 수용합니다.
우리는 ScheduledThreadPoolExecutor가 scheduleAtFixedRate로 구성된 플랫폼에서 다섯 초마다 서버의 생체 정보를 수집하는 분산 건강 모니터링 플랫폼을 운영했습니다. 데이터베이스 감소가 심각해지는 동안 메트릭 수집 쿼리가 30초 후에 타임아웃되었지만, 실행자는 대기열의 작업량과 관계없이 절대 일정에 따라 매 5초마다 새로운 작업을 계속 발사하여 작업 큐가 무한히 증가하고 OutOfMemoryError를 초래할 위기에 처했습니다.
접근 가능한 관찰 가능성을 유지하면서 시스템 붕괴를 방지하기 위해 여러 아키텍처 솔루션이 평가되었습니다. 누적된 대기열을 수용하기 위해 코어 풀 크기를 늘리는 것은 이미 실패한 데이터베이스에 압박을 가할 것이기 때문에 즉시 거부되었고, 이는 복구 중 우르르 몰려드는 문제를 일으키면서 메모리 소비를 가속시킬 것입니다. 데이터베이스가 비정상일 때 실행을 건너뛰는 회로 차단기를 구현하는 것은 운영적으로 실행 가능했지만 비즈니스 논리에 상당한 복잡성을 추가하며, 동시 스레드 간에 미세한 동기화 위험과 테스트의 어려움을 초래하는 공유 가변 상태가 필요했습니다. 최종적으로 scheduleWithFixedDelay로 전환하기로 결정했는데, 이는 추가 코드 복잡성 없이 본질적인 백프레셔를 제공했기 때문입니다: 작업이 30초를 소요할 때, 다음 실행은 완료 후 추가적으로 5초를 기다렸고, 이는 요청 간의 간격을 자연스럽게 확보하고 데이터베이스의 복구를 허용하여 자원 소모를 방지했습니다. 시스템은 중단 없이 사건 동안 안정되었고, 모니터링 대시보드에서는 역사적 데이터에서 비균일한 시간 간격이 드러났지만, 이는 연속적인 실패와 완전한 데이터 손실이라는 대안에 비해 수용 가능한 것으로 판단되었습니다.
두 작업이 동일한 실행 타임스탬프를 가질 때 내부 DelayedWorkQueue가 어떻게 순서를 유지하며, 이것이 고처리량 시나리오에서 명백한 스케줄링 공정성을 야기할 수 있는 이유는 무엇인가?
DelayedWorkQueue는 작업을 다음 실행 타임스탬프를 나타내는 time 필드에 따라 주로 정렬하는 이진 힙입니다. 타임스탬프가 충돌할 경우, 제출 시 할당된 단조롭게 증가하는 sequenceNumber 필드로 넘어가며, 이는 더 일찍 제출된 작업이 우선 순위를 받게 됩니다. 이 FIFO 동점 타결 방식은 풀 크기가 부족할 경우 긴 실행 시간을 가지는 주기적 작업의 기아를 초래할 수 있습니다. 왜냐하면 실행자는 힙에서 가장 짧은 대기 작업을 반복적으로 선택하게 되어 지연 작업이 대기열에 묻히게 되고 직관적인 라운드 로빈 기대를 위반하게 됩니다.
ScheduledThreadPoolExecutor가 하나의 runnable이 unchecked exception을 발생시킨 후에도 다른 예약 작업을 처리하는 이유는 무엇인가? java.util.Timer가 전체 스케줄링 스레드를 종료하는 것과는 달리?
Timer는 uncaught exception 발생 시 종료되는 단일 백그라운드 스레드를 사용하지만, ScheduledThreadPoolExecutor는 각 작업 실행이 **FutureTask.run()**을 통해 발생하는 스레드 풀 아키텍처를 활용합니다. 예외는 ScheduledFuture의 결과로 잡히고 저장되지만, 중요한 것은 작업자 스레드는 다치지 않은 채로 풀로 돌아가 다른 작업을 처리합니다. 특히 주기적 작업의 경우, **runAndReset()**이 예외로 인해 false를 반환하면 작업은 재예약되지 않지만, 스레드는 다른 보류 중인 일정 실행을 계속합니다. 이는 격리와 복원력을 제공합니다.
remove(Runnable)를 호출할 때, 메서드가 true를 반환한 후에도 실행자가 작업을 계속 실행할 수 있는 이유는 무엇이며, 동적 취소를 복잡하게 만드는 특정 식별 일치 동작은 무엇인가?
remove() 메서드는 관련 ScheduledFuture를 취소하고 DelayedWorkQueue에서 제거하려고 시도하지만, 이미 활성 실행 상태로 전환된 작업을 중단할 수는 없습니다. 또한 실행자는 제출된 runnable을 ScheduledFutureTask 객체로 래핑하기 때문에 **remove()**는 호출자가 전달한 원래 Runnable이 아닌 이러한 래퍼 인스턴스에 대해 식별 비교를 수행합니다. 개발자는 작업을 신뢰성 있게 취소하기 위해 스케줄링 메서드에서 반환된 ScheduledFuture를 유지해야 하며, 원래 runnable을 remove에 전달하면 일반적으로 내부 래퍼와의 참조 불일치로 인해 실패합니다.