Java에서 Schedule 사용하기

 

자바 어플리케이션에서 정기적으로 혹은 일회성 작업을 스케쥴링할 경우 Timer 클래스나 ScheduledExecutorService 클래스를 활용하여 thread-safe하게 사용할 수 있다.

공통 개념

Task

일련의 작업을 의미한다. 일반적으로 하나의 Task는 하나의 스레드에서 수행한다. 동일한 작업은 하나만 수행하므로, 멀티스레드 환경에서도 10초가 걸리는 작업을 1초마다 수행하도록 스케쥴하여도 이전 동일 작업이 끝나야 수행한다.

Delay

일련의 작업을 하기 위한 지연 시간을 의미한다. Timer의 경우 최초 지연 시간의 의미로 사용된다.

Period

일련의 작업을 주기적으로 사용하기 위한 기간(~마다)을 의미한다.

Fixed-delay

일련의 작업을 다시 스케쥴링을 하는 전략 중 하나로, 지연 시간을 고정으로 가져간다. 이는 해당 작업 이전의 다른 작업이 끝난 이후로 다시 스케쥴링한다. Timer, ScheduledExecutorService 모두 기본 전략으로 사용한다.

Fixed-delay

Fixed-rate

일련의 작업을 다시 스케쥴링을 하는 전략 중 하나로, 기간을 고정으로 가져간다. 이는 이전 동일 작업의 시작 시점을 기준으로 다시 스케쥴링한다.

Fixed-rate

Timer

단일 스레드를 생성하여 각 TimerTask 작업대기열에 추가하여 수행한다. 기본적은 시간 단위는 milliseconds를 사용한다.

public class Timer {

    private final TaskQueue queue = new TaskQueue();
    
    private final TimerThread thread = new TimerThread(queue);

}

Method

void schedule(TimerTask task, long delay) : task를 delay 이후 실행한다.

void schedule(TimerTask task, Date time) : task를 time에 실행한다.

void schedule(TimerTask task, long delay, long period) : fixed-delay 전략을 기반으로 task를 delay 이후 매번 period 마다 실행한다.

public void schedule(TimerTask task, Date firstTime, long period) : fixed-delay 전략을 기반으로 task를 최초 firstTime에 실행하며, 매번 period 마다 task를 실행한다.

void scheduleAtFixedRate(TimerTask task, long delay, long period) : fixed-rate 전략을 기반으로 task를 delay 이후 매번 period 마다 실행한다.

void scheduleAtFixedRate(TimerTask task, Date firstTime, long period) : fixed-rate 전략을 기반으로 task를 최초 firstTime에 실행하며, 매번 period 마다 task를 실행한다.

Example

@Test
public void timerTest() {
    Timer timer = new Timer();
    AtomicInteger number = new AtomicInteger();
    TimerTask timerTask = new TimerTask() {
        @Override
        public void run() {
            log.info("#{}", number.getAndIncrement());
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };
    
    timer.schedule(timerTask, 1000, 1000);
    Thread.sleep(10000);
}

/*
14:29:52.936 [Timer-0] INFO lab.yearnlune.TimerTest - #0
14:29:54.949 [Timer-0] INFO lab.yearnlune.TimerTest - #1
14:29:56.950 [Timer-0] INFO lab.yearnlune.TimerTest - #2
14:29:58.950 [Timer-0] INFO lab.yearnlune.TimerTest - #3
14:30:00.951 [Timer-0] INFO lab.yearnlune.TimerTest - #4
*/

주의점

여러 작업을 돌릴 경우 원하는 기간에 작업을 수행하지 못한다.

단일 스레드로 큐잉된 작업들을 순차적으로 수행하므로, fixed-delay, fixed-rate 모두 다른 작업 수행시간에 영향을 받아 원하는 기간마다 수행하도록 설정해도 원하는 기간에 수행할 수 없다.

@Test
public void 이전_작업이_긴_경우() throws Exception {
    Timer timer = new Timer();
    AtomicInteger totalSequence = new AtomicInteger(1);
    AtomicInteger taskASequence = new AtomicInteger(1);
    AtomicInteger taskBSequence = new AtomicInteger(1);
    
    TimerTask taskA = new TimerTask() {
        @Override
        public void run() {
            log.info("#{} A-{}", totalSequence.getAndIncrement(), taskASequence.getAndIncrement());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };
    
    TimerTask taskB = new TimerTask() {
        @Override
        public void run() {
            log.info("#{} B-{}", totalSequence.getAndIncrement(), taskBSequence.getAndIncrement());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };
    
    timer.schedule(taskA, 0, 2000);
    timer.schedule(taskB, 0, 2000);
    Thread.sleep(30000);
}

/*
17:21:08.639 [Timer-0] INFO lab.yearnlune.TimerTest - #1 A-1
17:21:11.647 [Timer-0] INFO lab.yearnlune.TimerTest - #2 B-1
17:21:12.647 [Timer-0] INFO lab.yearnlune.TimerTest - #3 B-2
17:21:13.649 [Timer-0] INFO lab.yearnlune.TimerTest - #4 A-2
17:21:16.649 [Timer-0] INFO lab.yearnlune.TimerTest - #5 B-3
17:21:17.650 [Timer-0] INFO lab.yearnlune.TimerTest - #6 B-4
17:21:18.650 [Timer-0] INFO lab.yearnlune.TimerTest - #7 A-3
17:21:21.651 [Timer-0] INFO lab.yearnlune.TimerTest - #8 B-5
17:21:22.652 [Timer-0] INFO lab.yearnlune.TimerTest - #9 B-6
17:21:23.653 [Timer-0] INFO lab.yearnlune.TimerTest - #10 B-7
17:21:24.653 [Timer-0] INFO lab.yearnlune.TimerTest - #11 A-4
17:21:27.653 [Timer-0] INFO lab.yearnlune.TimerTest - #12 B-8
17:21:28.654 [Timer-0] INFO lab.yearnlune.TimerTest - #13 B-9
17:21:29.655 [Timer-0] INFO lab.yearnlune.TimerTest - #14 B-10
17:21:30.655 [Timer-0] INFO lab.yearnlune.TimerTest - #15 A-5
17:21:33.656 [Timer-0] INFO lab.yearnlune.TimerTest - #16 B-11
17:21:34.657 [Timer-0] INFO lab.yearnlune.TimerTest - #17 B-12
17:21:35.657 [Timer-0] INFO lab.yearnlune.TimerTest - #18 B-13
17:21:36.658 [Timer-0] INFO lab.yearnlune.TimerTest - #19 A-6
*/

ScheduledExecutorService

스레드 풀을 통해서 idle 상태의 스레드를 활용하여 일련의 작업(Runnable)을 수행한다. 다시 말하자면, Timer 를 최대 corePoolSize만큼 수행하는 것과 같다.

Method

ScheduledFuture<?> schedule(Runnable task, long delay, TimeUnit unit) : task를 delay 이후 실행한다.

public ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, long initialDelay, long delay, TimeUnit unit) : fixed-delay 전략을 기반으로 task를 delay 이후 매번 period 마다 실행한다.

ScheduledFuture<?> scheduleAtFixedRate(Runnable task, long initialDelay, long period, TimeUnit unit) : fixed-rate 전략을 기반으로 task를 initialDelay 이후 매번 period 마다 실행한다.

Example

@Test
public void scheduledExecutorServiceTest() throws Exception {
    ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
    AtomicInteger number = new AtomicInteger();
    
    Runnable task = () -> {
        log.info("#{}", number.getAndIncrement());
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    };
    
    scheduledExecutorService.scheduleWithFixedDelay(task, 0, 1, TimeUnit.SECONDS);
    Thread.sleep(10000);
}

Leave a comment