Рубрики
Без рубрики

Как остановить выполнение Через определенное время в Java

Изучите различные способы завершения длительного выполнения через определенное время в Java

Автор оригинала: baeldung.

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() {
        try {
            while (!Thread.interrupted()) {
                Thread.sleep(500);
            }
        } catch (InterruptedException e) {
            // log error
        }
    }
}

Здесь Thread.sleep имитирует длительную операцию. Вместо этого может быть любая другая операция. Важно проверить флаг прерывания, потому что не все операции прерываются . Поэтому в этих случаях мы должны вручную проверить флаг.

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

Далее мы рассмотрим три различных механизма передачи сигнала прерывания.

3.1. Использование таймера

Кроме того, мы можем создать TimerTask для прерывания рабочего потока по таймауту:

class TimeOutTask extends TimerTask {
    private Thread t;
    private Timer timer;

    TimeOutTask(Thread t, Timer timer){
        this.t = t;
        this.timer = timer;
    }
 
    public void run() {
        if (t != null && t.isAlive()) {
            t.interrupt();
            timer.cancel();
        }
    }
}

Здесь мы определили a TimerTask , который принимает рабочий поток в момент его создания. Он прерывает рабочий поток при вызове его run метода . Timer вызовет TimerTask после указанной задержки:

Thread t = new Thread(new LongRunningTask());
Timer timer = new Timer();
timer.schedule(new TimeOutTask(t, timer), 30*1000);
t.start();

3.2. Использование метода Future#get

Мы также можем использовать метод get a Future вместо использования a Таймер :

ExecutorService executor = Executors.newSingleThreadExecutor();
Future future = executor.submit(new LongRunningTask());
try {
    f.get(30, TimeUnit.SECONDS);
} catch (TimeoutException e) {
    f.cancel(true);
} finally {
    service.shutdownNow();
}

Здесь мы использовали ExecutorService для отправки рабочего потока, который возвращает экземпляр Future , чей метод get заблокирует основной поток до указанного времени. Это поднимет TimeoutException после указанного таймаута. В блоке catch мы прерываем рабочий поток, вызывая метод cancel для объекта F uture .

Основное преимущество этого подхода по сравнению с предыдущим заключается в том, что он использует пул для управления потоком, в то время как Таймер использует только один поток (без пула) .

3.3. Использование Услуги Планового Исполнителя

Мы также можем использовать ScheduledExecutorService для прерывания задачи. Этот класс является расширением ExecutorService и предоставляет ту же функциональность с добавлением нескольких методов, которые имеют дело с планированием выполнения. Это может выполнить заданную задачу после определенной задержки в заданные единицы времени:

ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
Future future = executor.submit(new LongRunningTask());
executor.schedule(new Runnable(){
    public void run(){
        future.cancel(true);
    }
}, 1000, TimeUnit.MILLISECONDS);
executor.shutdown();

Здесь мы создали запланированный пул потоков размера два с помощью метода newScheduledThreadPool . Метод ScheduledExecutorService# schedule принимает Runnable , значение задержки и единицу задержки.

Приведенная выше программа планирует выполнение задачи через одну секунду с момента подачи. Эта задача отменит исходную длительную задачу.

Обратите внимание, что в отличие от предыдущего подхода, мы не блокируем основной поток, вызывая метод Future#get . Поэтому это наиболее предпочтительный подход среди всех вышеперечисленных подходов .

4. Есть ли гарантия?

Нет никакой гарантии, что выполнение будет остановлено через определенное время . Основная причина заключается в том, что не все методы блокировки прерываемы. На самом деле существует лишь несколько четко определенных методов, которые можно прервать. Таким образом, если поток прерывается и установлен флаг, ничего больше не произойдет, пока он не достигнет одного из этих прерываемых методов .

Например, методы read и write прерываются только в том случае, если они вызываются в потоках, созданных с помощью InterruptibleChannel . BufferedReader не является InterruptibleChannel . Таким образом, если поток использует его для чтения файла, вызов interrupt() в этом потоке, заблокированном в методе read , не имеет никакого эффекта.

Однако мы можем явно проверять наличие флага прерывания после каждого чтения в цикле. Это даст разумную гарантию остановить поток с некоторой задержкой. Но это не гарантирует остановку потока после строгого времени, потому что мы не знаем, сколько времени может занять операция чтения.

С другой стороны, метод wait класса Object прерываем. Таким образом, поток, заблокированный в методе wait , немедленно выдаст исключение InterruptedException после установки флага прерывания.

Мы можем идентифицировать методы блокировки, ища throws | InterruptedException в их сигнатурах методов.

Один важный совет заключается в том, чтобы избегать использования устаревшего метода Thread.stop () . Остановка потока приводит к тому, что он разблокирует все мониторы, которые он заблокировал. Это происходит из-за исключения Thread Death , которое распространяется вверх по стеку.

Если какой-либо из объектов, ранее защищенных этими мониторами, находился в несогласованном состоянии, несогласованные объекты становятся видимыми для других потоков. Это может привести к произвольному поведению, которое очень трудно обнаружить и объяснить.

5. Заключение

В этом уроке мы изучили различные методы остановки выполнения через определенное время, а также плюсы и минусы каждого из них. Полный исходный код можно найти на GitHub .