Как да спрем изпълнението след известно време в 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 е прекъсваем. По този начин нишката, блокирана в метода на изчакване , веднага ще изхвърли InterruptException, след като е зададен флагът за прекъсване.

Можем да идентифицираме методите за блокиране, като търсим хвърляния InterruptedException в техните подписи на методи.

Един важен съвет е да избягвате използването на остарелия метод Thread.stop () . Спирането на нишката води до отключване на всички монитори, които е заключил. Това се случва поради изключението ThreadDeath, което се разпространява нагоре по стека.

Ако някой от обектите, защитени преди това от тези монитори, е бил в несъвместимо състояние, непоследователните обекти стават видими за други нишки. Това може да доведе до произволно поведение, което е много трудно да се открие и да се разсъждава.

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

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