Як зупинити виконання через певний час у Java

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(); } } }

Тут ми визначили TimerTask, який бере робочий потік під час його створення. Це буде переривати робочий потік на виклик його виконання методу . Таймер викличе 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 майбутнього замість використання таймера :

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 після зазначеного тайм-ауту. В уловах блоку, ми перериваючи робочий потік з допомогою виклику скасувати метод на F uture об'єкті.

Основна перевага цього підходу в порівнянні з попереднім полягає в тому, що він використовує пул для управління потоком, тоді як таймер використовує лише один потік (без пулу) .

3.3. Використання ScheduledExcecutorSercvice

Ми також можемо використовувати 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 # графік приймає Runnable , значення затримки та одиницю затримки.

Вищевказана програма планує виконати завдання через одну секунду з моменту подання. Це завдання скасує початкове тривале завдання.

Зауважте, що на відміну від попереднього підходу, ми не блокуємо основний потік, викликаючи метод Future # get . Тому це найбільш бажаний підхід серед усіх згаданих вище підходів .

4. Чи є гарантія?

Немає жодної гарантії, що виконання буде зупинено через певний час . Основна причина полягає в тому, що не всі методи блокування є переривними. Насправді існує лише кілька чітко визначених методів, які є переривними. Отже, якщо потік перерваний і встановлений прапор, нічого іншого не трапиться, поки він не дійде до одного з цих методів переривання .

Наприклад, методи читання та запису переривні, лише якщо вони викликані в потоках, створених за допомогою InterruptibleChannel . BufferedReader не є InterruptibleChannel . Отже, якщо потік використовує його для читання файлу, виклик interrupt () для цього потоку, заблокованого в методі читання , не впливає.

Однак ми можемо явно перевірити прапор переривання після кожного читання в циклі. Це дасть розумну гарантію зупинити нитку з деякою затримкою. Але це не гарантує зупинки потоку через строгий час, оскільки ми не знаємо, скільки часу може зайняти операція зчитування.

З іншого боку, метод очікування класу Object є переривним. Таким чином, потік, заблокований у методі очікування , негайно видасть InterruptedException після встановлення прапора переривання.

Ми можемо визначити методи блокування, шукаючи викиди InterruptedException у їх підписах методів.

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

Якщо будь-який із об'єктів, раніше захищених цими моніторами, перебував у несумісному стані, невідповідні об'єкти стають видимими для інших потоків. Це може призвести до довільної поведінки, яку дуже важко виявити та міркувати.

5. Висновок

У цьому підручнику ми вивчили різні техніки зупинки виконання через певний час, а також плюси і мінуси кожного. Повний вихідний код можна знайти на GitHub.