java.util.concurrent ScheduledThreadPoolExecutor как замена классу Timer

от
Java    timer, concurrency

Класс Timer существует в Java уже очень давно и многие по привычке продолжают использовать его по сей день. Однако, у него есть некоторые неприятные особенности и ограничения. Об этом, а также о классе ScheduledThreadPoolExecutor, который служит заменой классу Timer я и расскажу.

Чем плох Timer?
1. Timer выполняет задачи только в одном потоке. Нет возможности изменить количество потоков или переиспользовать какой-то конкретный поток
2. Если задать несколько задач и при их выполнении одна из них бросит исключение, таймер прекратит свою работу и все оставшиеся задачи не будут выполнены.
3. Timer принимает задачи только в качестве экземпляра класса TimerTask, который кроме констант и пары методов ничего не предоставляет. Зато накладывается ограничение: задачей не могут быть классы, которые уже от чего-то наследованы. Приходится делать обёртку, а это не всегда удобно.
  1. import java.time.LocalTime;
  2. import java.util.Timer;
  3. import java.util.TimerTask;
  4. import java.util.concurrent.TimeUnit;
  5.  
  6. public final class TimerExample {
  7.  
  8.     public static void main(String[] args) {
  9.         Timer timer = new Timer();
  10.         for (int i = 1; i <= 10; i++) {
  11.             timer.schedule(new Task(), TimeUnit.SECONDS.toMillis(i));
  12.         }
  13.     }
  14.  
  15.     private static class Task extends TimerTask {
  16.  
  17.         @Override
  18.         public void run() {
  19.             final LocalTime now = LocalTime.now();
  20.             if (now.getSecond() % 10 == 0) {
  21.                 throw new IllegalStateException();
  22.             }
  23.             System.out.format("%s: %s%n", now, Thread.currentThread().getName());
  24.         }
  25.     }
  26. }
Вот простейший пример, который запускает 10 задач, которые должны будут выполниться через одну секунду, две, три и так далее до десяти. Каждую десятую секунду задача выбрасывает исключение. Вот что получается:
  1. 19:31:56.530932: Timer-0
  2. 19:31:57.448650: Timer-0
  3. 19:31:58.448307: Timer-0
  4. 19:31:59.448883: Timer-0
  5. Exception in thread "Timer-0" java.lang.IllegalStateException
  6.         at com.example.scheduledexecutor.TimerExample$Task.run(Timers.java:23)
  7.         at java.base/java.util.TimerThread.mainLoop(Timer.java:556)
  8.         at java.base/java.util.TimerThread.run(Timer.java:506)

ScheduledThreadPoolExecutor
В Java 1.5 в пакет java.util.concurrent добавили класс ScheduledThreadPoolExecutor, который имеет схожие методы и отныне его можно использовать как замену Timer.
  1. import java.time.LocalTime;
  2. import java.util.concurrent.Executors;
  3. import java.util.concurrent.ScheduledExecutorService;
  4. import java.util.concurrent.TimeUnit;
  5.  
  6. public class SchedulerExample {
  7.  
  8.     public static void main(String[] args) throws InterruptedException {
  9.         ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
  10.         for (int i = 1; i <= 10; i++) {
  11.             pool.schedule(SchedulerExample::task, i, TimeUnit.SECONDS);
  12.         }
  13.         pool.shutdown();
  14.         pool.awaitTermination(12, TimeUnit.SECONDS);
  15.     }
  16.  
  17.     private static void task() {
  18.         final LocalTime now = LocalTime.now();
  19.         if (now.getSecond() % 10 == 0) {
  20.             throw new IllegalStateException();
  21.         }
  22.         System.out.format("%s: %s%n", now, Thread.currentThread().getName());
  23.     }
  24. }
Теперь мы можем задавать количество потоков, которые будут переиспользоваться планировщиком. Теперь мы не ограничены классом TimerTask и можем передать Runnable или Callable в любом удобном виде, будь то классом, анонимным классом, лямбда-выражением, либо ссылкой на метод.

В данном примере мы создаём ScheduleExecutorService с двумя потоками в пуле: Executors.newScheduledThreadPool(2)
Затем запускаем 10 задач, как и в примере с таймером.
Затем метод pool.shutdown() запрещает добавление новых задач в ExecutorService, а pool.awaitTermination(12, TimeUnit.SECONDS); ждёт 12 секунд, чтобы все задачи завершились.
  1. 19:37:16.749194: pool-1-thread-1
  2. 19:37:17.694984: pool-1-thread-2
  3. 19:37:18.695194: pool-1-thread-1
  4. 19:37:19.698077: pool-1-thread-2
  5. 19:37:21.698027: pool-1-thread-2
  6. 19:37:22.698584: pool-1-thread-1
  7. 19:37:23.698037: pool-1-thread-2
  8. 19:37:24.698138: pool-1-thread-1
  9. 19:37:25.698034: pool-1-thread-2
На этот раз задача, выполнявшая на двадцатой секунде, не погубила остальные запланированные задачи. Также мы видим, что планирощик переиспользует два потока.

Обработка результата и исключений в задачах
А что, если запланированная задача должна вернуть результат? Или тот факт, что в ней произошло исключение, тоже является результатом? На этот случай предусмотрен метод, принимающий Callable:
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);

Для нас это означает лишь то, что нужно в лямбда-выражении добавить return, либо в методе — возвращаемое значение, тогда ссылка на метод будет соответствовать интерфейсу Callable.
  1. import java.time.LocalTime;
  2. import java.util.concurrent.ExecutionException;
  3. import java.util.concurrent.Executors;
  4. import java.util.concurrent.ScheduledExecutorService;
  5. import java.util.concurrent.ScheduledFuture;
  6. import java.util.concurrent.TimeUnit;
  7.  
  8. public class SchedulerCallableExample {
  9.  
  10.     public static void main(String[] args) throws InterruptedException {
  11.         ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
  12.         ScheduledFuture<LocalTime> future = pool.schedule(SchedulerCallableExample::task, 3, TimeUnit.SECONDS);
  13.         pool.shutdown();
  14.         System.out.format("Now: %s%n", LocalTime.now());
  15.         try {
  16.             System.out.format("Future: %s%n", future.get());
  17.         } catch (ExecutionException ex) {
  18.             System.err.println(ex.getMessage());
  19.         }
  20.         pool.awaitTermination(5, TimeUnit.SECONDS);
  21.     }
  22.  
  23.     private static LocalTime task() {
  24.         final LocalTime now = LocalTime.now();
  25.         if (now.getSecond() % 10 == 0) {
  26.             throw new IllegalStateException();
  27.         }
  28.         return now;
  29.     }
  30. }
Если задача завершена успешно, мы получим:
  1. Now: 21:48:38.717335
  2. Future: 21:48:41.674347

Если произошла ошибка, будет выброшено ExecutionException с содержимым оригинального исключения и получим такой вывод:
  1. Now: 21:48:47.653664
  2. java.lang.IllegalStateException

Напомню, get() — блокирующая операция, которая ожидает завершения задачи.

Отмена задач
У класса TimerTask был метод cancel(), который позволял повторяющейся задаче отменить саму себя, а у класса Timer был метод purge, который отменял все задачи.
Теперь же для отмены задачи можно бросить исключение, либо добавить ещё одну задачу, которая отменит повторяющуюся:
  1. public static void main(String[] args) throws InterruptedException, ExecutionException {
  2.     ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
  3.     ScheduledFuture<?> future = pool.scheduleAtFixedRate(() -> {
  4.         System.out.format("%s: %s%n", LocalTime.now(), Thread.currentThread().getName());
  5.     }, 2, 5, TimeUnit.SECONDS);
  6.     pool.schedule(() -> {
  7.         System.out.println("Cancel fixed rate task and shutdown");
  8.         future.cancel(true);
  9.         pool.shutdown();
  10.     }, 20, TimeUnit.SECONDS);
  11. }

Мы запускаем задачу, которая спустя две секунды будет повторяться каждые пять секунд (2, 7, 12, 17, …). Затем ещё одну, которая спустя 20 секунд отменит первую.
  1. 22:18:55.550069: pool-1-thread-1
  2. 22:19:00.493999: pool-1-thread-1
  3. 22:19:05.493851: pool-1-thread-1
  4. 22:19:10.493861: pool-1-thread-1
  5. Cancel fixed rate task and shutdown

Примеры
scheduleWithFixedDelay
scheduleWithFixedDelay, как следует из названия, делает задержку между задачами. Если задержка 2 секунды, а задача выполняется 6 секунд, то следующая будет запущена на 8 секунде. Если вторая задача выполняется 3 секунды, то третья запустится через 5 секунд после старта второй.
  1. public class SchedulerFixedDelayExample {
  2.  
  3.     public static void main(String[] args) throws InterruptedException {
  4.         ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
  5.         Runnable task = SchedulerFixedDelayExample::task;
  6.         ScheduledFuture<?> future = pool.scheduleWithFixedDelay(task, 0, 2, TimeUnit.SECONDS);
  7.         pool.schedule(() -> {
  8.             future.cancel(true);
  9.             pool.shutdown();
  10.         }, 20, TimeUnit.SECONDS);
  11.     }
  12.  
  13.     private static void task() {
  14.         final LocalTime now = LocalTime.now();
  15.         final int sleepTime = 2 + now.toSecondOfDay() % 5;
  16.         System.out.format("Start at %s. Sleep for %d seconds. ", now, sleepTime);
  17.         try {
  18.             Thread.sleep(sleepTime * 1000);
  19.         } catch (InterruptedException ex) {
  20.             Thread.currentThread().interrupt();
  21.         }
  22.         System.out.format("Done at %s!%n", LocalTime.now());
  23.     }
  24. }
  1. Start at 22:41:18.928241. Sleep for 5 seconds. Done at 22:41:23.943473!
  2. Start at 22:41:25.944234. Sleep for 2 seconds. Done at 22:41:27.944795!
  3. Start at 22:41:29.945459. Sleep for 6 seconds. Done at 22:41:35.945959!
  4. Start at 22:41:37.946597. Sleep for 4 seconds. Done at 22:41:38.817652!

Как видно, между завершением одной задачи и стартом второй всегда задержка в две секунды.

scheduleAtFixedRate
В отличие от scheduleWithFixedDelay, scheduleAtFixedRate старается выполнять задачи с одинаковой частотой. Но если текущая задача ещё не завершилась, а время для старта новой уже подошло, то планировщик будет ждать окончания задачи, после чего сразу запустит следующую.
Если частота 4 секунды, первая задача выполняется за 1 секунду, то вторая будет запущена через 4 секунды после старта первой (или через 3 после завершения первой). Если вторая задача выполнялась 6 секунд, то третья будет запущена сразу же после завершения второй.
  1. public class SchedulerFixedRateExample {
  2.  
  3.     public static void main(String[] args) throws InterruptedException {
  4.         ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
  5.         Runnable task = SchedulerFixedRateExample::task;
  6.         ScheduledFuture<?> future = pool.scheduleAtFixedRate(task, 0, 4, TimeUnit.SECONDS);
  7.         pool.schedule(() -> {
  8.             future.cancel(true);
  9.             pool.shutdown();
  10.         }, 20, TimeUnit.SECONDS);
  11.     }
  12.  
  13.     private static void task() {
  14.         final LocalTime now = LocalTime.now();
  15.         final int sleepTime = 2 + now.toSecondOfDay() % 5;
  16.         System.out.format("Start at %s. Sleep for %d seconds. ", now, sleepTime);
  17.         try {
  18.             Thread.sleep(sleepTime * 1000);
  19.         } catch (InterruptedException ex) {
  20.             Thread.currentThread().interrupt();
  21.         }
  22.         System.out.format("Done at %s!%n", LocalTime.now());
  23.     }
  24. }
  1. Start at 22:45:00.502951. Sleep for 2 seconds. Done at 22:45:02.520166!
  2. Start at 22:45:04.433061. Sleep for 6 seconds. Done at 22:45:10.433670!
  3. Start at 22:45:10.434091. Sleep for 2 seconds. Done at 22:45:12.434534!
  4. Start at 22:45:12.434986. Sleep for 4 seconds. Done at 22:45:16.435477!
  5. Start at 22:45:16.435918. Sleep for 3 seconds. Done at 22:45:19.436401!
  6. Start at 22:45:20.433069. Sleep for 2 seconds. Done at 22:45:20.438451!



Исходный код: https://github.com/annimon-tut...eadPoolExecutor-Demo
  • +6
  • views 10201