1. Обзор
В этой статье мы узнаем, как мы можем завершить длительное выполнение через определенное время. Мы рассмотрим различные решения этой проблемы. Кроме того, мы рассмотрим некоторые их подводные камни.
2. Использование цикла
Представьте, что мы обрабатываем кучу элементов в цикле, например, некоторые сведения об элементах продукта в приложении электронной коммерции, но, возможно, нет необходимости заполнять все элементы.
Фактически, мы хотели бы обрабатывать только до определенного времени, а после этого мы хотим остановить выполнение и показать все, что список обработал до этого времени.
Давайте посмотрим на быстрый пример:
long start = System.currentTimeMillis();
long end = start + 30 * 1000;
while (System.currentTimeMillis() < end) {
// Some expensive operation on the item.
}
Здесь цикл прервется, если время превысит ограничение в 30 секунд. В приведенном выше решении есть несколько заслуживающих внимания моментов:
- Низкая точность: цикл может работать дольше установленного ограничения по времени . Это будет зависеть от времени, которое может занять каждая итерация. Например, если каждая итерация может занять до 7 секунд, то общее время может увеличиться до 35 секунд, что примерно на 17% больше, чем желаемое ограничение времени в 30 секунд.
- Блокировка: такая обработка в основном потоке может быть не очень хорошей идеей, поскольку она будет блокировать ее на долгое время . Вместо этого эти операции должны быть отделены от основного потока.
В следующем разделе мы обсудим, как подход, основанный на прерываниях, устраняет эти ограничения.
3. Использование механизма прерывания
Здесь мы будем использовать отдельный поток для выполнения длительных операций. Основной поток отправит сигнал прерывания рабочему потоку по тайм-ауту.
Если рабочий поток все еще жив, он поймает сигнал и остановит свое выполнение. Если рабочий процесс завершится до истечения времени ожидания, это не повлияет на рабочий поток.
Давайте посмотрим на рабочий поток:
class LongRunningTask implements Runnable {
@Override
public void run() {
for (int i = 0; i < Long.MAX_VALUE; i++) {
if(Thread.interrupted()) {
return;
}
}
}
}
Здесь цикл for через Long.MAX_VALUE
`` имитирует длительную операцию. Вместо этого может быть любая другая операция. Важно проверять флаг прерывания, потому что не все операции прерываемы . Поэтому в таких случаях мы должны вручную проверять флаг.
Кроме того, мы должны проверять этот флаг на каждой итерации, чтобы убедиться, что поток прекращает выполнение самого себя с задержкой не более одной итерации.
Далее мы рассмотрим три различных механизма отправки сигнала прерывания.
3.1. Использование таймера
В качестве альтернативы мы можем создать TimerTask
для прерывания рабочего потока по истечении времени ожидания:
class TimeOutTask extends TimerTask {
private Thread thread;
private Timer timer;
public TimeOutTask(Thread thread, Timer timer) {
this.thread = thread;
this.timer = timer;
}
@Override
public void run() {
if(thread != null && thread.isAlive()) {
thread.interrupt();
timer.cancel();
}
}
}
Здесь мы определили TimerTask
, который принимает рабочий поток во время его создания. Он прервет рабочий поток при вызове его метода run
. Таймер запустит TimerTask
после трехсекундной
задержки:
Thread thread = new Thread(new LongRunningTask());
thread.start();
Timer timer = new Timer();
TimeOutTask timeOutTask = new TimeOutTask(thread, timer);
timer.schedule(timeOutTask, 3000);
3.2. Использование метода Future#get
Мы также можем использовать метод get
Future
вместо использования Timer
:
ExecutorService executor = Executors.newSingleThreadExecutor();
Future future = executor.submit(new LongRunningTask());
try {
future.get(7, TimeUnit.SECONDS);
} catch (TimeoutException e) {
future.cancel(true);
} catch (Exception e) {
// handle other exceptions
} finally {
executor.shutdownNow();
}
Здесь мы использовали ExecutorService
для отправки рабочего потока, который возвращает экземпляр Future
, чей метод get
заблокирует основной поток до указанного времени. Это вызовет исключение TimeoutException
после указанного тайм-аута. В блоке catch
мы прерываем рабочий поток, вызывая метод отмены для
объекта
Future
.
Основное преимущество этого подхода по сравнению с предыдущим заключается в том, что он использует пул для управления потоком, в то время как Timer
использует только один поток (без пула) .
3.3. Использование ScheduledExcecutorService
Мы также можем использовать ScheduledExecutorService
для прерывания задачи. Этот класс является расширением ExecutorService
и предоставляет ту же функциональность с добавлением нескольких методов, которые имеют дело с планированием выполнения. Это может выполнить данную задачу после определенной задержки в заданных единицах времени:
ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
Future future = executor.submit(new LongRunningTask());
Runnable cancelTask = () -> future.cancel(true);
executor.schedule(cancelTask, 3000, TimeUnit.MILLISECONDS);
executor.shutdown();
Здесь мы создали запланированный пул потоков размером два с помощью метода newScheduledThreadPool
. Метод расписания
ScheduledExecutorService#
принимает Runnable
, значение задержки и единицу измерения задержки. [
](/lessons/b/-java-runnable-vs-extending-thread)
Приведенная выше программа планирует выполнение задачи через три секунды с момента отправки. Эта задача отменит исходную длительную задачу.
Обратите внимание, что, в отличие от предыдущего подхода, мы не блокируем основной поток, вызывая метод Future#get
. Таким образом, это наиболее предпочтительный подход среди всех вышеупомянутых подходов .
4. Есть ли гарантия?
Нет никакой гарантии, что выполнение будет остановлено через определенное время . Основная причина в том, что не все методы блокировки прерываемы. На самом деле существует лишь несколько четко определенных прерываемых методов. Итак, если поток прерван и установлен флаг, ничего больше не произойдет, пока он не достигнет одного из этих прерываемых методов .
Например, методы чтения
и записи
являются прерываемыми, только если они вызываются для потоков, созданных с помощью InterruptibleChannel
. BufferedReader
не является InterruptibleChannel
. Таким образом, если поток использует его для чтения файла, вызов прерывания()
в этом потоке, заблокированном в методе чтения
, не имеет никакого эффекта.
Однако мы можем явно проверять наличие флага прерывания после каждого чтения в цикле. Это даст разумную гарантию остановки потока с некоторой задержкой. Но это не гарантирует остановки потока по истечении заданного времени, потому что мы не знаем, сколько времени может занять операция чтения.
С другой стороны, метод ожидания класса
Object
можно прерывать. Таким образом, поток, заблокированный в методе ожидания
, немедленно вызовет InterruptedException
после установки флага прерывания.
Мы можем определить методы блокировки, найдя
в сигнатурах их методов исключение InterruptedException .
``
Один важный совет — избегать использования устаревшего метода Thread.stop()
. Остановка потока заставляет его разблокировать все мониторы, которые он заблокировал. Это происходит из-за исключения ThreadDeath
, которое распространяется вверх по стеку.
Если какой-либо из объектов, ранее защищенных этими мониторами, находился в несогласованном состоянии, несогласованные объекты становятся видимыми для других потоков. Это может привести к произвольному поведению, которое очень трудно обнаружить и обосновать.
5. Дизайн для прерывания
В предыдущем разделе мы подчеркнули важность прерываемых методов для скорейшей остановки выполнения. Поэтому наш код должен учитывать это ожидание с точки зрения дизайна.
Представьте, что нам нужно выполнить длительную задачу, и нам нужно убедиться, что она не займет больше времени, чем указано. Кроме того, предположим, что задачу можно разделить на отдельные шаги.
Создадим класс для шагов задачи:
class Step {
private static int MAX = Integer.MAX_VALUE/2;
int number;
public Step(int number) {
this.number = number;
}
public void perform() throws InterruptedException {
Random rnd = new Random();
int target = rnd.nextInt(MAX);
while (rnd.nextInt(MAX) != target) {
if (Thread.interrupted()) {
throw new InterruptedException();
}
}
}
}
Здесь метод Step#perform
пытается найти целевое случайное целое число, запрашивая флаг на каждой итерации. Метод выдает InterruptedException
при активации флага.
Теперь давайте определим задачу, которая будет выполнять все шаги:
public class SteppedTask implements Runnable {
private List<Step> steps;
public SteppedTask(List<Step> steps) {
this.steps = steps;
}
@Override
public void run() {
for (Step step : steps) {
try {
step.perform();
} catch (InterruptedException e) {
// handle interruption exception
return;
}
}
}
}
Здесь у SteppedTask
есть список шагов для выполнения. Цикл for выполняет каждый шаг и обрабатывает InterruptedException
для остановки задачи, когда она возникает.
Наконец, давайте посмотрим на пример использования нашей прерываемой задачи:
List<Step> steps = Stream.of(
new Step(1),
new Step(2),
new Step(3),
new Step(4))
.collect(Collectors.toList());
Thread thread = new Thread(new SteppedTask(steps));
thread.start();
Timer timer = new Timer();
TimeOutTask timeOutTask = new TimeOutTask(thread, timer);
timer.schedule(timeOutTask, 10000);
Сначала мы создаем SteppedTask
с четырьмя шагами. Во-вторых, мы запускаем задачу, используя поток. Наконец, мы прерываем поток через десять секунд, используя таймер и задачу тайм-аута.
Благодаря этому дизайну мы можем гарантировать, что наша длительная задача может быть прервана при выполнении любого шага. Как мы видели ранее, недостатком является то, что нет гарантии, что она остановится точно в указанное время, но, безусловно, это лучше, чем непрерываемая задача.
6. Заключение
В этом уроке мы изучили различные методы остановки выполнения через заданное время, а также плюсы и минусы каждого из них. Полный исходный код можно найти на GitHub .