실행 방법입니다. 외부 jar 는 필요 없습니다.
cd java-src\extension\09_scheduled
C:\project\jdk-21.0.8\bin\javac -encoding UTF-8 *.java
C:\project\jdk-21.0.8\bin\java -Dstdout.encoding=UTF-8 Main # 7개 시나리오, 약 6초// Main.fixedRateVsFixedDelay
AtomicInteger rate = new AtomicInteger(), delay = new AtomicInteger();
Runnable work = () -> sleep(300); // 작업 300ms
ses.scheduleAtFixedRate(
() -> { work.run(); rate.incrementAndGet(); },
0, 200, TimeUnit.MILLISECONDS); // 간격 200ms
ses.scheduleWithFixedDelay(
() -> { work.run(); delay.incrementAndGet(); },
0, 200, TimeUnit.MILLISECONDS);
sleep(1500);fixedRate 실행 5회 ← 밀린 만큼 연달아 실행(간격 0)
fixedDelay 실행 3회 ← 300+200 = 500ms 마다작업(300ms)이 간격(200ms)보다 길어서 차이가 드러났습니다. fixedRate 는 밀린 회차를 쌓아 두고 연달아 소진하고, fixedDelay 는 항상 500ms 를 지킵니다.
// Main.exceptionKillsSchedule
Runnable boom = () -> {
if (raw.incrementAndGet() == 3)
throw new IllegalStateException("DB 연결 끊김");
};
ScheduledFuture<?> f = ses.scheduleAtFixedRate(
boom, 0, 100, TimeUnit.MILLISECONDS);
SafeTask st = new SafeTask("safe",
() -> { if (safe.incrementAndGet() == 3)
throw new IllegalStateException("DB 연결 끊김"); },
Duration.ofMillis(50));
ses.scheduleAtFixedRate(st, 0, 100, TimeUnit.MILLISECONDS);
sleep(1000);날것 실행 3회, isDone=true ← 3회 후 영구 정지, 로그 없음
숨어 있던 예외: f.get() → java.lang.IllegalStateException: DB 연결 끊김
safe: 성공 9, 건너뜀 0, 실패 1 ← 실패해도 다음 회차 계속SafeTask.run 전체는 짧습니다.
// SafeTask.run
@Override
public void run() {
if (!running.compareAndSet(false, true)) { // 이전 회차 진행 중
skips.incrementAndGet();
log("SKIP 이전 실행이 아직 진행 중");
return;
}
Instant start = Instant.now();
try {
body.run();
runs.incrementAndGet();
Duration took = Duration.between(start, Instant.now());
log((took.compareTo(slowThreshold) > 0 ? "SLOW " : "OK ")
+ took.toMillis() + "ms");
} catch (Throwable t) { // Error 까지 잡아야 안 죽는다
failures.incrementAndGet();
log("FAIL " + t);
} finally {
running.set(false);
}
}AtomicBoolean running 이 겹침 방지, catch (Throwable t) 가 예외 방지, finally 가 다음 회차를 위한 원복입니다. 세 줄짜리 아이디어를 한 클래스에 모아 두면 모든 배치에 재사용할 수 있습니다.
// Main.singleThreadBlocksOthers
for (int threads : new int[]{1, 4}) {
ScheduledExecutorService ses =
Executors.newScheduledThreadPool(threads, named("sch"));
ses.scheduleAtFixedRate(() -> sleep(800),
0, 1000, TimeUnit.MILLISECONDS); // 느린 작업
ses.scheduleAtFixedRate(() -> { // 빠른 작업, 간격을 측정
long now = System.currentTimeMillis();
maxGap[0] = Math.max(maxGap[0], now - last[0]);
last[0] = now;
}, 0, 100, TimeUnit.MILLISECONDS);
sleep(1000);
}스레드 1개: 빠른 작업 10회, 최대 간격 817ms ← 느린 작업 뒤에 갇혔다가 몰아서 실행
스레드 4개: 빠른 작업 11회, 최대 간격 119ms ← 100ms 간격 유지스레드 1개 풀에서는 느린 작업이 스레드를 점유한 800ms 동안 빠른 작업이 실행되지 못하고 밀립니다. 스레드를 4개로 늘리면 서로 방해하지 않습니다.
// Main.overlapGuard
SafeTask task = new SafeTask("report", () -> sleep(250),
Duration.ofMillis(500));
ScheduledExecutorService a =
Executors.newSingleThreadScheduledExecutor(named("A"));
ScheduledExecutorService b =
Executors.newSingleThreadScheduledExecutor(named("B"));
a.scheduleAtFixedRate(task, 0, 300, TimeUnit.MILLISECONDS);
b.scheduleAtFixedRate(task, 100, 300, TimeUnit.MILLISECONDS);
sleep(600);[B-1] report SKIP 이전 실행이 아직 진행 중
[A-1] report OK 259ms
[B-1] report SKIP 이전 실행이 아직 진행 중
[A-1] report OK 267ms
[A-1] report OK 15ms
report: 성공 3, 건너뜀 2, 실패 0같은 SafeTask 인스턴스를 스케줄러 A·B 가 각각 돌렸습니다. running 플래그가 두 스케줄러에 공유되므로, A 가 실행 중일 때 B 의 회차는 SKIP 됩니다. 인스턴스는 하나인데 스케줄러가 둘인 상황을 흉내 낸 예제입니다.