java.util.concurrent ScheduledThreadPoolExecutor как замена классу Timer
от aNNiMON
Класс Timer существует в Java уже очень давно и многие по привычке продолжают использовать его по сей день. Однако, у него есть некоторые неприятные особенности и ограничения. Об этом, а также о классе ScheduledThreadPoolExecutor, который служит заменой классу Timer я и расскажу.
Чем плох Timer?
1. Timer выполняет задачи только в одном потоке. Нет возможности изменить количество потоков или переиспользовать какой-то конкретный поток
2. Если задать несколько задач и при их выполнении одна из них бросит исключение, таймер прекратит свою работу и все оставшиеся задачи не будут выполнены.
3. Timer принимает задачи только в качестве экземпляра класса TimerTask, который кроме констант и пары методов ничего не предоставляет. Зато накладывается ограничение: задачей не могут быть классы, которые уже от чего-то наследованы. Приходится делать обёртку, а это не всегда удобно.
Вот простейший пример, который запускает 10 задач, которые должны будут выполниться через одну секунду, две, три и так далее до десяти. Каждую десятую секунду задача выбрасывает исключение. Вот что получается:
ScheduledThreadPoolExecutor
В Java 1.5 в пакет java.util.concurrent добавили класс ScheduledThreadPoolExecutor, который имеет схожие методы и отныне его можно использовать как замену Timer.
Теперь мы можем задавать количество потоков, которые будут переиспользоваться планировщиком. Теперь мы не ограничены классом TimerTask и можем передать Runnable или Callable в любом удобном виде, будь то классом, анонимным классом, лямбда-выражением, либо ссылкой на метод.
В данном примере мы создаём ScheduleExecutorService с двумя потоками в пуле: Executors.newScheduledThreadPool(2)
Затем запускаем 10 задач, как и в примере с таймером.
Затем метод pool.shutdown() запрещает добавление новых задач в ExecutorService, а pool.awaitTermination(12, TimeUnit.SECONDS); ждёт 12 секунд, чтобы все задачи завершились.
На этот раз задача, выполнявшая на двадцатой секунде, не погубила остальные запланированные задачи. Также мы видим, что планирощик переиспользует два потока.
Обработка результата и исключений в задачах
А что, если запланированная задача должна вернуть результат? Или тот факт, что в ней произошло исключение, тоже является результатом? На этот случай предусмотрен метод, принимающий Callable:
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);
Для нас это означает лишь то, что нужно в лямбда-выражении добавить return, либо в методе — возвращаемое значение, тогда ссылка на метод будет соответствовать интерфейсу Callable.
Если задача завершена успешно, мы получим:
Если произошла ошибка, будет выброшено ExecutionException с содержимым оригинального исключения и получим такой вывод:
Напомню, get() — блокирующая операция, которая ожидает завершения задачи.
Отмена задач
У класса TimerTask был метод cancel(), который позволял повторяющейся задаче отменить саму себя, а у класса Timer был метод purge, который отменял все задачи.
Теперь же для отмены задачи можно бросить исключение, либо добавить ещё одну задачу, которая отменит повторяющуюся:
Мы запускаем задачу, которая спустя две секунды будет повторяться каждые пять секунд (2, 7, 12, 17, …). Затем ещё одну, которая спустя 20 секунд отменит первую.
Примеры
scheduleWithFixedDelay
scheduleWithFixedDelay, как следует из названия, делает задержку между задачами. Если задержка 2 секунды, а задача выполняется 6 секунд, то следующая будет запущена на 8 секунде. Если вторая задача выполняется 3 секунды, то третья запустится через 5 секунд после старта второй.
Как видно, между завершением одной задачи и стартом второй всегда задержка в две секунды.
scheduleAtFixedRate
В отличие от scheduleWithFixedDelay, scheduleAtFixedRate старается выполнять задачи с одинаковой частотой. Но если текущая задача ещё не завершилась, а время для старта новой уже подошло, то планировщик будет ждать окончания задачи, после чего сразу запустит следующую.
Если частота 4 секунды, первая задача выполняется за 1 секунду, то вторая будет запущена через 4 секунды после старта первой (или через 3 после завершения первой). Если вторая задача выполнялась 6 секунд, то третья будет запущена сразу же после завершения второй.
Исходный код: https://github.com/annimon-tut...eadPoolExecutor-Demo
Чем плох Timer?
1. Timer выполняет задачи только в одном потоке. Нет возможности изменить количество потоков или переиспользовать какой-то конкретный поток
2. Если задать несколько задач и при их выполнении одна из них бросит исключение, таймер прекратит свою работу и все оставшиеся задачи не будут выполнены.
3. Timer принимает задачи только в качестве экземпляра класса TimerTask, который кроме констант и пары методов ничего не предоставляет. Зато накладывается ограничение: задачей не могут быть классы, которые уже от чего-то наследованы. Приходится делать обёртку, а это не всегда удобно.
- import java.time.LocalTime;
- import java.util.Timer;
- import java.util.TimerTask;
- import java.util.concurrent.TimeUnit;
- public final class TimerExample {
- public static void main(String[] args) {
- Timer timer = new Timer();
- for (int i = 1; i <= 10; i++) {
- timer.schedule(new Task(), TimeUnit.SECONDS.toMillis(i));
- }
- }
- private static class Task extends TimerTask {
- @Override
- public void run() {
- final LocalTime now = LocalTime.now();
- if (now.getSecond() % 10 == 0) {
- throw new IllegalStateException();
- }
- System.out.format("%s: %s%n", now, Thread.currentThread().getName());
- }
- }
- }
- 19:31:56.530932: Timer-0
- 19:31:57.448650: Timer-0
- 19:31:58.448307: Timer-0
- 19:31:59.448883: Timer-0
- Exception in thread "Timer-0" java.lang.IllegalStateException
- at com.example.scheduledexecutor.TimerExample$Task.run(Timers.java:23)
- at java.base/java.util.TimerThread.mainLoop(Timer.java:556)
- at java.base/java.util.TimerThread.run(Timer.java:506)
ScheduledThreadPoolExecutor
В Java 1.5 в пакет java.util.concurrent добавили класс ScheduledThreadPoolExecutor, который имеет схожие методы и отныне его можно использовать как замену Timer.
- import java.time.LocalTime;
- import java.util.concurrent.Executors;
- import java.util.concurrent.ScheduledExecutorService;
- import java.util.concurrent.TimeUnit;
- public class SchedulerExample {
- public static void main(String[] args) throws InterruptedException {
- ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
- for (int i = 1; i <= 10; i++) {
- pool.schedule(SchedulerExample::task, i, TimeUnit.SECONDS);
- }
- pool.shutdown();
- pool.awaitTermination(12, TimeUnit.SECONDS);
- }
- private static void task() {
- final LocalTime now = LocalTime.now();
- if (now.getSecond() % 10 == 0) {
- throw new IllegalStateException();
- }
- System.out.format("%s: %s%n", now, Thread.currentThread().getName());
- }
- }
В данном примере мы создаём ScheduleExecutorService с двумя потоками в пуле: Executors.newScheduledThreadPool(2)
Затем запускаем 10 задач, как и в примере с таймером.
Затем метод pool.shutdown() запрещает добавление новых задач в ExecutorService, а pool.awaitTermination(12, TimeUnit.SECONDS); ждёт 12 секунд, чтобы все задачи завершились.
- 19:37:16.749194: pool-1-thread-1
- 19:37:17.694984: pool-1-thread-2
- 19:37:18.695194: pool-1-thread-1
- 19:37:19.698077: pool-1-thread-2
- 19:37:21.698027: pool-1-thread-2
- 19:37:22.698584: pool-1-thread-1
- 19:37:23.698037: pool-1-thread-2
- 19:37:24.698138: pool-1-thread-1
- 19:37:25.698034: pool-1-thread-2
Обработка результата и исключений в задачах
А что, если запланированная задача должна вернуть результат? Или тот факт, что в ней произошло исключение, тоже является результатом? На этот случай предусмотрен метод, принимающий Callable:
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);
Для нас это означает лишь то, что нужно в лямбда-выражении добавить return, либо в методе — возвращаемое значение, тогда ссылка на метод будет соответствовать интерфейсу Callable.
- import java.time.LocalTime;
- import java.util.concurrent.ExecutionException;
- import java.util.concurrent.Executors;
- import java.util.concurrent.ScheduledExecutorService;
- import java.util.concurrent.ScheduledFuture;
- import java.util.concurrent.TimeUnit;
- public class SchedulerCallableExample {
- public static void main(String[] args) throws InterruptedException {
- ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
- ScheduledFuture<LocalTime> future = pool.schedule(SchedulerCallableExample::task, 3, TimeUnit.SECONDS);
- pool.shutdown();
- System.out.format("Now: %s%n", LocalTime.now());
- try {
- System.out.format("Future: %s%n", future.get());
- } catch (ExecutionException ex) {
- System.err.println(ex.getMessage());
- }
- pool.awaitTermination(5, TimeUnit.SECONDS);
- }
- private static LocalTime task() {
- final LocalTime now = LocalTime.now();
- if (now.getSecond() % 10 == 0) {
- throw new IllegalStateException();
- }
- return now;
- }
- }
- Now: 21:48:38.717335
- Future: 21:48:41.674347
Если произошла ошибка, будет выброшено ExecutionException с содержимым оригинального исключения и получим такой вывод:
- Now: 21:48:47.653664
- java.lang.IllegalStateException
Напомню, get() — блокирующая операция, которая ожидает завершения задачи.
Отмена задач
У класса TimerTask был метод cancel(), который позволял повторяющейся задаче отменить саму себя, а у класса Timer был метод purge, который отменял все задачи.
Теперь же для отмены задачи можно бросить исключение, либо добавить ещё одну задачу, которая отменит повторяющуюся:
- public static void main(String[] args) throws InterruptedException, ExecutionException {
- ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
- ScheduledFuture<?> future = pool.scheduleAtFixedRate(() -> {
- System.out.format("%s: %s%n", LocalTime.now(), Thread.currentThread().getName());
- }, 2, 5, TimeUnit.SECONDS);
- pool.schedule(() -> {
- System.out.println("Cancel fixed rate task and shutdown");
- future.cancel(true);
- pool.shutdown();
- }, 20, TimeUnit.SECONDS);
- }
Мы запускаем задачу, которая спустя две секунды будет повторяться каждые пять секунд (2, 7, 12, 17, …). Затем ещё одну, которая спустя 20 секунд отменит первую.
- 22:18:55.550069: pool-1-thread-1
- 22:19:00.493999: pool-1-thread-1
- 22:19:05.493851: pool-1-thread-1
- 22:19:10.493861: pool-1-thread-1
- Cancel fixed rate task and shutdown
Примеры
scheduleWithFixedDelay
scheduleWithFixedDelay, как следует из названия, делает задержку между задачами. Если задержка 2 секунды, а задача выполняется 6 секунд, то следующая будет запущена на 8 секунде. Если вторая задача выполняется 3 секунды, то третья запустится через 5 секунд после старта второй.
- public class SchedulerFixedDelayExample {
- public static void main(String[] args) throws InterruptedException {
- ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
- Runnable task = SchedulerFixedDelayExample::task;
- ScheduledFuture<?> future = pool.scheduleWithFixedDelay(task, 0, 2, TimeUnit.SECONDS);
- pool.schedule(() -> {
- future.cancel(true);
- pool.shutdown();
- }, 20, TimeUnit.SECONDS);
- }
- private static void task() {
- final LocalTime now = LocalTime.now();
- final int sleepTime = 2 + now.toSecondOfDay() % 5;
- System.out.format("Start at %s. Sleep for %d seconds. ", now, sleepTime);
- try {
- Thread.sleep(sleepTime * 1000);
- } catch (InterruptedException ex) {
- Thread.currentThread().interrupt();
- }
- System.out.format("Done at %s!%n", LocalTime.now());
- }
- }
- Start at 22:41:18.928241. Sleep for 5 seconds. Done at 22:41:23.943473!
- Start at 22:41:25.944234. Sleep for 2 seconds. Done at 22:41:27.944795!
- Start at 22:41:29.945459. Sleep for 6 seconds. Done at 22:41:35.945959!
- Start at 22:41:37.946597. Sleep for 4 seconds. Done at 22:41:38.817652!
Как видно, между завершением одной задачи и стартом второй всегда задержка в две секунды.
scheduleAtFixedRate
В отличие от scheduleWithFixedDelay, scheduleAtFixedRate старается выполнять задачи с одинаковой частотой. Но если текущая задача ещё не завершилась, а время для старта новой уже подошло, то планировщик будет ждать окончания задачи, после чего сразу запустит следующую.
Если частота 4 секунды, первая задача выполняется за 1 секунду, то вторая будет запущена через 4 секунды после старта первой (или через 3 после завершения первой). Если вторая задача выполнялась 6 секунд, то третья будет запущена сразу же после завершения второй.
- public class SchedulerFixedRateExample {
- public static void main(String[] args) throws InterruptedException {
- ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
- Runnable task = SchedulerFixedRateExample::task;
- ScheduledFuture<?> future = pool.scheduleAtFixedRate(task, 0, 4, TimeUnit.SECONDS);
- pool.schedule(() -> {
- future.cancel(true);
- pool.shutdown();
- }, 20, TimeUnit.SECONDS);
- }
- private static void task() {
- final LocalTime now = LocalTime.now();
- final int sleepTime = 2 + now.toSecondOfDay() % 5;
- System.out.format("Start at %s. Sleep for %d seconds. ", now, sleepTime);
- try {
- Thread.sleep(sleepTime * 1000);
- } catch (InterruptedException ex) {
- Thread.currentThread().interrupt();
- }
- System.out.format("Done at %s!%n", LocalTime.now());
- }
- }
- Start at 22:45:00.502951. Sleep for 2 seconds. Done at 22:45:02.520166!
- Start at 22:45:04.433061. Sleep for 6 seconds. Done at 22:45:10.433670!
- Start at 22:45:10.434091. Sleep for 2 seconds. Done at 22:45:12.434534!
- Start at 22:45:12.434986. Sleep for 4 seconds. Done at 22:45:16.435477!
- Start at 22:45:16.435918. Sleep for 3 seconds. Done at 22:45:19.436401!
- Start at 22:45:20.433069. Sleep for 2 seconds. Done at 22:45:20.438451!
Исходный код: https://github.com/annimon-tut...eadPoolExecutor-Demo