본문 바로가기

java

timer schedule vs scheduleAtFixedRate

[Timer] schedule vs scheduleAtFixedRate 예제로 이해하기


타이머를 이용하여 백그라운드 스레드에서 반복적인 작업을 수행

많은 응용프로그램들이 나중에 수행할 작업을 스케쥴하거나, 일정한 간격으로 반복적으로 수행하는 것을 필요로 합니다. J2SE v1.3에서는 두개의 추가된 Timer 클래스-java.util.Timerjava.util.TimerTask-를 통해 이것을 지원합니다. 이 팁은 이러한 타이머 클래스들을 이용하는 다양한 스케쥴 전략을 보여줍니다. 또한 잘 동작하지 않는 작업, 즉 너무 오래동안 수행되거나 또는 깨지는 작업을 핸들링하는 방법을 보여줍니다.

java.util.Timer와 java.util.TimerTask 클래스는 사용하기가 쉽습니다. 많은 것들이 스레드로 동작하듯, TimerTask 클래스는 Runnable인터페이스를 구현합니다. 이 클래스를 이용하기 위해서는 실제 작업을 수행하는 run 메소드를 구현하는 하위 클래스를 만들면 됩니다. 그리고 이것을 Timer의 인스턴스에 붙입니다. 아래에 예가 있습니다.

    import java.util.*;
    import java.io.*;
    
    public class TestTimers
    {
        public static void doMain() throws Exception
        {
            Timer t = new Timer(true);
            t.schedule(new Ping("Fixed delay"), 0, 1000);
            Thread.currentThread().sleep(12000);
        }
    
        public static void main(String[] args)
        {
            try
            { 
                doMain(); 
            }
            catch (Exception e)
            {
                e.printStackTrace();
            }
        }
    }
    
    class Ping extends TimerTask
    {
        private String name;
        public Ping(String name)
        {
            this.name = name;
        }
        public void run()
        {
            System.out.println(name + " Ping at " + new Date());
        }
    }

TestTimers 클래스는 Timer를 생성합니다. Timer에 true 값을 전달함으로써, TestTimers는 Timer를 데몬 스레드로 사용하도록 합니다. 그리고 메인 스레드는 타이머가 동작하는 것을 보여주기 위해 sleep 합니다. 그러나 실제로 어떤 스레드 클래스나 인스턴스도 볼 수 없습니다. 이런 것들은 Timer 클래스에 캡슐화되어 있습니다.

문장 내부에

    t.schedule(new Ping("Fixed delay"), 0, 1000);

schedule 메소드의 인자들은 Ping 객체의 run 메소드를 초기 지연시간인 0/1000초 후에 호출하도록 하며, 그뒤로 1000/1000초마다 반복해서 호출합니다. Ping의 run 메소드는 출력을 System.out을 통해 기록합니다. (run 메소드에서 더 흥미있는 일을 할 수 있습니다.) TestTimers를 실행하면 아래와 비슷한 결과를 얻을 수 있습니다.

    Fixed delay ping at Thu May 18 14:18:56 EDT 2000
    Fixed delay ping at Thu May 18 14:18:57 EDT 2000
    Fixed delay ping at Thu May 18 14:18:58 EDT 2000
    Fixed delay ping at Thu May 18 14:18:59 EDT 2000
    Fixed delay ping at Thu May 18 14:19:00 EDT 2000
    Fixed delay ping at Thu May 18 14:19:01 EDT 2000
    Fixed delay ping at Thu May 18 14:19:02 EDT 2000
    Fixed delay ping at Thu May 18 14:19:03 EDT 2000
    Fixed delay ping at Thu May 18 14:19:04 EDT 2000
    Fixed delay ping at Thu May 18 14:19:05 EDT 2000
    Fixed delay ping at Thu May 18 14:19:06 EDT 2000
    Fixed delay ping at Thu May 18 14:19:07 EDT 2000
    Fixed delay ping at Thu May 18 14:19:08 EDT 2000

이런 결과는 Ping이 요청한대로 정확히 1초마다 동작하고 있다는 것을 보여줍니다. 더 좋은것은, Timer가 여러개의 TimerTask를 각각 서로 다른 시작시간과 주기를 가지고 조절할 수 있다는 것입니다. 여기서 흥미로운 질문이 하나 생깁니다. 만일 TimerTask가 완료하기까지 시간이 오래걸리는 작업을 수행한다면, 목록에 들어있는 다른 작업들은 버리게 되는가?

이 질문에 답변을 위해, Timer가 어떻게 스레드를 사용하는지 이해할 필요가 있습니다. 각 Timer 인스턴스는 모든 TimerTask가 공유하는 하나의 스레드를 가지고 있습니다. 만일 하나의 작업이 긴 시간을 소모한다면, 다른 모든 작업들은 이것이 완료되기를 기다립니다. 아래와 같은 시간이 오래 걸리는 작업을 가정해 봅시다.

    class PainstakinglySlowTask extends TimerTask
    {
        public void run()
	{
            // sleep을 이용해 시간이 느린 작업을 시뮬레이트합니다.
            try
	    {
                Thread.currentThread().sleep(6000);
                System.out.println("Painstaking task ran at " + new Date());
            }
            catch (InterruptedException ie)
	    {
                Thread.currentThread().interrupt();
            }
        }
    }

PainstakingSlowTask 클래스는 6초를 모두 sleep 합니다. 이것은 이 시간동안 다른 어떤 작업도 수행되는 것을 막습니다. TestTimers에 이런 고통스러울 정도로 느린 작업을 추가하면 어떻게 될까요? 봅시다.

    public static void doMain() throws Exception
    {
        Timer t = new Timer(true);
        t.schedule(new Ping("Fixed delay"), 0, 1000);
        t.schedule(new PainstakinglySlowTask(), 2000);
        Thread.currentThread().sleep(12000);
    }

다시 컴파일하고 TestTimers를 실행하면, 아래와 같은 결과를 볼 수 있습니다.

    Fixed delay Ping at Thu May 18 15:41:33 EDT 2000
    Fixed delay Ping at Thu May 18 15:41:34 EDT 2000
    Fixed delay Ping at Thu May 18 15:41:35 EDT 2000
    Painstaking task ran at Thu May 18 15:41:41 EDT 2000
    Fixed delay Ping at Thu May 18 15:41:41 EDT 2000
    Fixed delay Ping at Thu May 18 15:41:42 EDT 2000
    Fixed delay Ping at Thu May 18 15:41:43 EDT 2000
    Fixed delay Ping at Thu May 18 15:41:44 EDT 2000
    Fixed delay Ping at Thu May 18 15:41:45 EDT 2000

PainstakinglySlowTask가 실행되는 시간(15:41:35부터 15:41:41까지)동안 ping은 호출되지 않습니다. 이것이 바로 "고정된 지연"의 의미입니다. Timer는 Ping 사이의 간격을 다른 시간이 오래 걸리는 작업에 의해 잃게 되더라도 가능한 한 정확히 맞추려고 합니다.

스케쥴링의 대안은 "고정된 비율"입니다. 고정 비율 스케쥴링에서, Timer는 실행하는 동안 가능한 한 정확한 작업 비율을 맞추려고 합니다. 따라서, 하나의 작업이 너무 오래동안 수행되면, 다른 작업들은 순간적으로 이것을 만회하기 위해 여러번의 수행을 합니다. schedueAtFixedRate 메소드를 이용하여 고정 비율 스케쥴링 모드로 지정할 수 있습니다.

    // 고정 지연 버젼을 주석화합니다.
    // t.schedule(new Ping("Fixed delay"), 0, 1000);
    t.scheduleAtFixedRate(new Ping("Fixed rate"), 0, 1000);
    t.schedule(new PainstakinglySlowTask(), 2000);

TestTimers를 고정 비율로 실행하면 아래와 같은 결과를 얻을 수 있습니다.

    Fixed rate Ping at Thu May 18 15:48:33 EDT 2000
    Fixed rate Ping at Thu May 18 15:48:34 EDT 2000
    Fixed rate Ping at Thu May 18 15:48:35 EDT 2000
    Painstaking task ran at Thu May 18 15:48:41 EDT 2000
    Fixed rate Ping at Thu May 18 15:48:41 EDT 2000
    Fixed rate Ping at Thu May 18 15:48:41 EDT 2000
    Fixed rate Ping at Thu May 18 15:48:41 EDT 2000
    Fixed rate Ping at Thu May 18 15:48:41 EDT 2000
    Fixed rate Ping at Thu May 18 15:48:41 EDT 2000
    Fixed rate Ping at Thu May 18 15:48:41 EDT 2000
    Fixed rate Ping at Thu May 18 15:48:42 EDT 2000
    Fixed rate Ping at Thu May 18 15:48:43 EDT 2000
    Fixed rate Ping at Thu May 18 15:48:44 EDT 2000
    Fixed rate Ping at Thu May 18 15:48:45 EDT 2000

이번에는, PainstakinglySlowTask가 끝남과 동시에 몇개의 Ping이 수행되었습니다. 모든 Ping은 15:48:41에 수행되었습니다. 이것은 Ping의 비율을 1000/1000초마다 수행하기 위한 비율에 가까이 맞춰줍니다. 이런 경우 치러야 하는 대가는 Ping이 거의 동시에 수행된다는 것입니다.

고정 비율과 고정 지연 스케쥴링은 모두 각각의 용도가 있습니다. 그러나, 수행시간이 긴 작업으로 인한 방해는 완전히 없앨 수 없습니다. 만일 아주 긴 시간동안 수행해야 하는 다른 작업이 있다면, 작업간의 방해를 최소화하기 원할것입니다. 이것은 특별히 다중 CPU의 장점을 얻고자 할때 더 필요합니다. 하나의 Timer는 이렇게 할 수 있는 방법이 없습니다. 사용자는 Timer 클래스의 스레드가 Timer 클래스 내부에 private 필드로 캡슐화되어 있기 때문에 이것을 조정할 수 없습니다. 대신, 여러개의 Timer를 생성하거나, 하나의 Timer가 notify()를 호출하고 다른 스레드가 실제 작업을 처리하도록 할 수 있습니다.

예외상황을 발생시키는 작업은 오랜 시간동안 수행되는 작업보다 더 큰 문제가 있습니다. 아래 예가 있습니다. PainstakinglySlowTask클래스를 아래의 CrashingTask 클래스로 바꾸십시오.

    class CrashingTask extends TimerTask
    {
        public void run()
        {
            throw new Error("CrashingTask");
        }
    }

    // TestTimers의 새 버젼
    public static void doMain() throws Exception
    {
        Timer t = new Timer(true);
        t.scheduleAtFixedRate(new Ping("Fixed rate"), 0, 1000);
        t.schedule(new CrashingTask(), 5000, 1000);
        Thread.currentThread().sleep(12000);
    }

TestTimers를 CrashingTask와 함께 실행하면, 아래와 같은 결과를 볼 수 있습니다.

    Fixed rate Ping at Thu May 18 15:58:53 EDT 2000
    Fixed rate Ping at Thu May 18 15:58:54 EDT 2000
    Fixed rate Ping at Thu May 18 15:58:55 EDT 2000
    Fixed rate Ping at Thu May 18 15:58:56 EDT 2000
    Fixed rate Ping at Thu May 18 15:58:57 EDT 2000
    Fixed rate Ping at Thu May 18 15:58:58 EDT 2000
    java.lang.Error: CrashingTask
        at CrashingTask.run(TestTimers.java:37)
        at java.util.TimerThread.mainLoop(Timer.java:435)
        at java.util.TimerThread.run(Timer.java:385)

CrashingTask가 예외상황을 발생한 후, 이것은 다시 실행되지 않습니다. 이것은 놀라운 일이 아닙니다. 놀라운 것은 같은 Timer를 이용하는 다른 작업들도 다시는 수행되지 않는다는 것입니다. 뜻밖의 예외상황이 Timer를 취소해 버립니다. 그러나 다른 작업들에게 그것들이 무자비하게 스케쥴이 취소되었다는 것을 알릴 방법이 없습니다. 잘못된 TimerTask가 Timer를 파괴하지 않도록 하는 것은 전적으로 개발자의 책임입니다. 한가지 방법은 TimerTask가 Timer로 예외상황을 전달하지 않도록 하는 것입니다. 이것은 TimerTask를 예외상황을 잡아내기 위해 try 블럭으로 감싸는 것입니다. 만일 예외상황을 알려야 할 필요가 있다면, 프로그램에 알리는 간단한 장치를 만들어 놓을 수 있습니다. 여기 예제가 있습니다.

    import java.util.*;
    import java.io.*;
    
    interface ExceptionListener
    {
        public void exceptionOccurred(Throwable t);
    }
    
    class ExceptionLogger implements ExceptionListener
    {
        public void exceptionOccurred(Throwable t)
        {
            System.err.println("Exception on Timer thread!");
            t.printStackTrace();
        }
    }
    
    public class TestTimers
    {
        public static void doMain() throws Exception
        {
            Timer t = new Timer(true);
            //t.schedule(new Ping("Fixed delay"), 0, 1000);
            t.scheduleAtFixedRate(new Ping("Fixed rate"), 0, 1000);
            t.schedule(new CrashingTask(new ExceptionLogger()), 5000, 5000);
            //t.schedule(new PainstakinglySlowTask(), 2000);
            Thread.currentThread().sleep(12000);
        }
    
        public static void main(String[] args)
        {
            try
            { 
                doMain(); 
            }
            catch (Exception e)
            {
                e.printStackTrace();
            }
        }
    }
    
    class Ping extends TimerTask
    {
        private String name;
        public Ping(String name)
        {
            this.name = name;
        }
        public void run()
        {
            System.out.println(name + " Ping at " + new Date());
        }
    }
    
    class CrashingTask extends TimerTask
    {
        ExceptionListener el;
        public CrashingTask(ExceptionListener el)
        {
            this.el = el;
        }
    
        public void run()
        {
            try
            {
                throw new Error("CrashingTask");
            }
            catch (Throwable t)
            {
                cancel();
                el.exceptionOccurred(t);
            }
        }
    }

이 코드는 CrashingTask의 run 메소드가 예외상황을 전달하지 않는 다는것을 제외하면 앞의 버젼과 매우 유사합니다. 대신, 모든 Throwable 예외상황을 잡아내고 이 예외상황을 callback 인터페이스를 이용해 보고하기 위해 catch 블럭을 이용합니다. 아래에 그 결과가 있습니다.

    Fixed rate Ping at Thu May 18 16:41:03 EDT 2000
    Fixed rate Ping at Thu May 18 16:41:04 EDT 2000
    Fixed rate Ping at Thu May 18 16:41:05 EDT 2000
    Fixed rate Ping at Thu May 18 16:41:06 EDT 2000
    Fixed rate Ping at Thu May 18 16:41:07 EDT 2000
    Fixed rate Ping at Thu May 18 16:41:08 EDT 2000
    Exception on Timer thread!
    java.lang.Error: CrashingTask
        at CrashingTask.run(TestTimers.java:54)
        at java.util.TimerThread.mainLoop(Timer.java:435)
        at java.util.TimerThread.run(Timer.java:385)
    Fixed rate Ping at Thu May 18 16:41:09 EDT 2000
    Fixed rate Ping at Thu May 18 16:41:10 EDT 2000
    Fixed rate Ping at Thu May 18 16:41:11 EDT 2000
    Fixed rate Ping at Thu May 18 16:41:12 EDT 2000
    Fixed rate Ping at Thu May 18 16:41:13 EDT 2000
    Fixed rate Ping at Thu May 18 16:41:14 EDT 2000
    Fixed rate Ping at Thu May 18 16:41:15 EDT 2000

CrashingTask가 예외상황을 발생하면, 자기 자신의 cancel을 호출하여 Timer로부터 자신을 제거합니다. 그리고 ExceptionListener 인터페이스의 구현을 호출함으로써 예외상황을 기록합니다. 예외상황이 절대로 Timer 스레드로 전달되지 않기 때문에, Ping들은 CrashingTask가 잘못되더라도 계속해서 기능을 수행합니다. 판매하기 위한 시스템에서는 더 잘 만들어진 ExceptionListener가 간단하게 기록하는것 대신 예외상황을 다루기 위한 동작을 취합니다.

자바 플랫폼에는 또 다른 Timer 클래스인 javax.swing.Timer가 있습니다. 어떤 Timer를 이용하시겠습니까? Swing Timer는 아주 특정한 목적을 위해 설계되었습니다. 이것은 AWT 이벤트 스레드에 대해 동작합니다. 대부분의 Swing 패키지 코드가 AWT 이벤트 스레드 위에서 동작해야 하기 때문에, Swing 타이머는 사용자 인터페이스를 조작할 때에 이용해야 합니다. 다른 작업에서는 유연한 스케쥴링을 위해 java.util.Timer를 이용하십시오.

Timer 클래스에 대한 더 자세한 정보는 http://java.sun.com/j2se/1.3/docs/api/java/util/Timer.html를 참고하십시오.


Local Test

public static void main(String[] args) {

Timer t = new Timer();

t.schedule(new TimerTask() {

private int a = 0;

public void run() {

if ( a > 10 ) {

try {

Thread.sleep(7000);

} catch ( Exception e ) {

e.printStackTrace();

}

}

a++;

System.out.println(new Date() + " <<< date");

}

}, 1, 5000);

}

=====================

Fri Jul 08 10:09:47 KST 2016 <<< date

Fri Jul 08 10:09:52 KST 2016 <<< date

Fri Jul 08 10:09:57 KST 2016 <<< date

Fri Jul 08 10:10:02 KST 2016 <<< date

Fri Jul 08 10:10:07 KST 2016 <<< date

Fri Jul 08 10:10:12 KST 2016 <<< date

Fri Jul 08 10:10:17 KST 2016 <<< date

Fri Jul 08 10:10:22 KST 2016 <<< date

Fri Jul 08 10:10:27 KST 2016 <<< date

Fri Jul 08 10:10:32 KST 2016 <<< date

Fri Jul 08 10:10:37 KST 2016 <<< date      지연 시작

Fri Jul 08 10:10:49 KST 2016 <<< date      

Fri Jul 08 10:10:56 KST 2016 <<< date

Fri Jul 08 10:11:03 KST 2016 <<< date

Fri Jul 08 10:11:10 KST 2016 <<< date

Fri Jul 08 10:11:17 KST 2016 <<< date

Fri Jul 08 10:11:24 KST 2016 <<< date

Fri Jul 08 10:11:31 KST 2016 <<< date

Fri Jul 08 10:11:38 KST 2016 <<< date

Fri Jul 08 10:11:45 KST 2016 <<< date

Fri Jul 08 10:11:52 KST 2016 <<< date

Fri Jul 08 10:11:59 KST 2016 <<< date

Fri Jul 08 10:12:06 KST 2016 <<< date

'java' 카테고리의 다른 글

현재 돌고 있는 threadpool 리스트 보기  (0) 2016.07.04
ehcache 캐시와 연동하기  (0) 2016.06.15
java.exe와 javaw.exe  (0) 2016.06.10
thread exception  (0) 2016.06.01
was listener  (0) 2016.04.18