Как приостановить выполнение программы java
В этой статье мы узнаем, как можно завершить длительное выполнение через определенное время. Мы рассмотрим различные варианты решения этой проблемы. Кроме того, мы рассмотрим некоторые из их ловушек.
2. Использование петли
Представьте себе, что мы обрабатываем кучу элементов в цикле, например, некоторые детали элементов продукта в приложении электронной коммерции, но, возможно, нет необходимости заполнять все элементы.
На самом деле мы хотели бы обрабатывать только до определенного времени, а после этого мы хотим остановить выполнение и показать все, что список обработал до этого времени.
Давайте рассмотрим краткий пример:
Здесь петля разорвется, если время превысит предел в 30 секунд. В приведенном выше решении есть несколько примечательных моментов:
- Низкая точность: Цикл может работать дольше, чем установленный лимит времени . Это будет зависеть от времени, которое может занять каждая итерация. Например, если каждая итерация может занять до 7 секунд, то общее время может увеличиться до 35 секунд, что примерно на 17% больше, чем желаемый лимит времени в 30 секунд
- Блокировка: Такая обработка в основном потоке может быть не очень хорошей идеей, так как она будет блокировать его в течение длительного времени . Вместо этого эти операции должны быть отделены от основного потока
В следующем разделе мы обсудим, как подход, основанный на прерываниях, устраняет эти ограничения.
3. Использование механизма прерывания
Здесь мы будем использовать отдельный поток для выполнения длительных операций. Основной поток будет посылать сигнал прерывания рабочему потоку по таймауту.
Если рабочий поток все еще жив, он поймает сигнал и остановит его выполнение. Если рабочий завершит работу до истечения тайм-аута, это никак не повлияет на рабочий поток.
Давайте взглянем на рабочий поток:
Здесь Thread.sleep имитирует длительную операцию. Вместо этого может быть любая другая операция. Важно проверить флаг прерывания, потому что не все операции прерываются . Поэтому в этих случаях мы должны вручную проверить флаг.
Кроме того, мы должны проверять этот флаг на каждой итерации, чтобы убедиться, что поток прекращает выполнение самого себя в течение задержки не более одной итерации.
Далее мы рассмотрим три различных механизма передачи сигнала прерывания.
3.1. Использование таймера
Кроме того, мы можем создать TimerTask для прерывания рабочего потока по таймауту:
Здесь мы определили a TimerTask , который принимает рабочий поток в момент его создания. Он прерывает рабочий поток при вызове его run метода . Timer вызовет TimerTask после указанной задержки:
Мы также можем использовать метод get a Future вместо использования a Таймер :
Здесь мы использовали ExecutorService для отправки рабочего потока, который возвращает экземпляр Future , чей метод get заблокирует основной поток до указанного времени. Это поднимет TimeoutException после указанного таймаута. В блоке catch мы прерываем рабочий поток, вызывая метод cancel для объекта F uture .
Основное преимущество этого подхода по сравнению с предыдущим заключается в том, что он использует пул для управления потоком, в то время как Таймер использует только один поток (без пула) .
3.3. Использование Услуги Планового Исполнителя
Мы также можем использовать ScheduledExecutorService для прерывания задачи. Этот класс является расширением ExecutorService и предоставляет ту же функциональность с добавлением нескольких методов, которые имеют дело с планированием выполнения. Это может выполнить заданную задачу после определенной задержки в заданные единицы времени:
Приведенная выше программа планирует выполнение задачи через одну секунду с момента подачи. Эта задача отменит исходную длительную задачу.
4. Есть ли гарантия?
Нет никакой гарантии, что выполнение будет остановлено через определенное время . Основная причина заключается в том, что не все методы блокировки прерываемы. На самом деле существует лишь несколько четко определенных методов, которые можно прервать. Таким образом, если поток прерывается и установлен флаг, ничего больше не произойдет, пока он не достигнет одного из этих прерываемых методов .
Например, методы read и write прерываются только в том случае, если они вызываются в потоках, созданных с помощью InterruptibleChannel . BufferedReader не является InterruptibleChannel . Таким образом, если поток использует его для чтения файла, вызов interrupt() в этом потоке, заблокированном в методе read , не имеет никакого эффекта.
Однако мы можем явно проверять наличие флага прерывания после каждого чтения в цикле. Это даст разумную гарантию остановить поток с некоторой задержкой. Но это не гарантирует остановку потока после строгого времени, потому что мы не знаем, сколько времени может занять операция чтения.
С другой стороны, метод wait класса Object прерываем. Таким образом, поток, заблокированный в методе wait , немедленно выдаст исключение InterruptedException после установки флага прерывания.
Мы можем идентифицировать методы блокировки, ища throws | InterruptedException в их сигнатурах методов.
Один важный совет заключается в том, чтобы избегать использования устаревшего метода Thread.stop () . Остановка потока приводит к тому, что он разблокирует все мониторы, которые он заблокировал. Это происходит из-за исключения Thread Death , которое распространяется вверх по стеку.
Если какой-либо из объектов, ранее защищенных этими мониторами, находился в несогласованном состоянии, несогласованные объекты становятся видимыми для других потоков. Это может привести к произвольному поведению, которое очень трудно обнаружить и объяснить.
5. Заключение
В этом уроке мы изучили различные методы остановки выполнения через определенное время, а также плюсы и минусы каждого из них. Полный исходный код можно найти на GitHub .
— Привет, Амиго! Согласись, Элли хорошо придумала с этим Cancel?
— На самом деле нечто подобное существует в классе Thread. Только переменная называется не isCancel, а isInterrupted, и метод остановки, соответственно, не cancel(), а interrupt().
Класс Clock (часы) будет писать в консоль раз в секунду слово «Tik», пока переменная isInterrupted текущей нити равна false.
Ждет 10 секунд и отменяет задание, вызовом метода interrupt.
Главная нить завершает свою работу.
Более того, в методе sleep, который так любят использовать для организации вечного цикла в методе run, есть автоматическая проверка переменной isInterrupted. Если нить вызовет метод sleep, то этот метод сначала проверит, а не установлена ли для текущей (вызвавшей его нити) переменная isInterrupted в true. И если установлена, то метод не будет спать, а выкинет исключение InterruptedException .
— А зачем выкидывать исключение? Не лучше ли тоже просто в цикле вместо isCancel подставить isInterrupted()?
— Во-первых, не всегда в методе run есть цикл. Метод может состоять просто из двух десятков вызовов других методов. Тогда перед вызовом каждого придется добавлять проверку isInterrupted.
Во-вторых, вдруг какой-то метод очень долго исполняется, т.к. делает много разных действий.
В-третьих, выкидывание исключения – это не замена проверке isInterrupted, а скорее удобное дополнение. Выкинутое исключение позволяет быстро раскрутить стек вызовов до самого run.
В-четвертых, метод sleep часто используют, и, получается, к такому полезному методу неявно добавили не менее полезную проверку. Вроде бы никто специально проверку не добавлял, а она есть. Это очень ценно, когда ты используешь много чужого кода и не можешь сам добавить в него проверку.
В-пятых, дополнительная проверка не приводит к снижению производительности. Вызов метода sleep значит, что нить должна ничего не делать (спать), поэтому дополнительная работа никому не мешает.
— И, наконец, последнее: ты можешь в своем методе run вызывать чужой код, к которому у тебя нет доступа (исходников и/или прав их менять). Он может не иметь проверок на isInterrupted, а также перехватывать с помощью try…catch(Exception e) все возникшие исключения.
Никто не гарантирует, что нить можно остановить. Она может остановиться только сама.
В работу программы на Java можно вставлять паузы. Обычно это не нужно, т.к. пользователи хотят, чтобы их программы работали как можно быстрее. И вряд ли кто-то из них обрадуется, если вы будете специально замедлять свой код.
Но у вас как у программиста может быть куча ситуаций, когда пауза в коде может быть полезной. Например, вы программируете игру и хотите, чтобы она делала какие-то действия раз в две секунды или несколько раз в секунду.
В общем, пауза вещь полезная, поэтому смотрим, как же добавить паузу в код. Это на самом деле очень просто:
Где время — это длина паузы в миллисекундах ( 1/1000 часть секунды).
Исполнение такой команды приостановит вашу программу на время миллисекунд. Примеры:
Приостановит программу на 2 секунды. |
Приостановит программу на полсекунды. |
Приостановит программу на 1 час. |
Вот как это можно использовать на практике. Допустим, мы пишем программу, которая бы запускала космический корабль. Так бы мог выглядеть ее код:
2. Правильный расчет пауз
Длину паузы считать легко. Если вам нужно, чтобы программа что-то делала раз в секунду, поставьте паузу — 1000 мс. Если 2 раза в секунду, поставьте паузу 500 мс (1000/2).
Если нужно выполнять что-то 15 раз в секунду, паузу делаем длинной 66 мс (1000/15). Вроде бы все очень просто:
Но тут есть очень важный нюанс. Хотя многие команды выполняются очень быстро, они все же выполняются не мгновенно.
Смотрите. Допустим, у вас есть какое-то действие, выполнение которого занимает 100 мс. Вы хотите, чтобы оно выполнялось 5 раз в секунду. Какую длину паузы вам нужно выбрать? Точно не 200 мс.
Чтобы действие выполнялось 5 раз в секунду, нужно чтобы время выполнения действия + пауза равнялись 200 мс. Тогда оно действительно будет выполняться 5 раз в секунду. В нашем случае действие выполняется 100 мс, значит на паузу остается еще 100 мс
То, что время выполнения действия гораздо выше нуля, хорошо понимают разработчики игр. Хотя и люди, играющие в игры, тоже.
Если у игры 20 FPS, это значит, что за одну секунду она успевает отрисовать на экране всего 20 кадров. 1000/20 — получаем 50 мс. Именно столько длится время отрисовки кадра игры .
Yield
Метод Thread.yield() загадочный и редко используемый. Существует много вариаций его описания в интернете. Вплоть до того, что некоторые пишут про какую-то очередь потоков, в которой поток переместится вниз с учётом их приоритетов. Кто-то пишет, что поток изменит статус с running на runnable (хотя разделения на эти статусы нет, и Java их не различает). Но на самом деле всё куда неизвестнее и в каком-то смысле проще. На тему документации метода yield есть баг "JDK-6416721 : (spec thread) Fix Thread.yield() javadoc". Если прочитать его, то понятно, что на самом деле метод yield лишь передаёт некоторую рекомендацию планировщику потоков Java, что данному потоку можно дать меньше времени исполнения. Но что будет на самом деле, услышит ли планировщик рекомендацию и что вообще он будет делать — зависит от реализации JVM и операционной системы. А может и ещё от каких-то других факторов. Вся путаница сложилась, скорее всего, из-за переосмысления многопоточности в процессе развития языка Java. Подробнее можно прочитать в обзоре "Brief Introduction to Java Thread.yield()".
Sleep - Засыпание потока
Поток в процессе своего выполнения может засыпать. Это самой простой тип взаимодействия с другими потоками. В операционной системе, на которой установлена виртуальная Java машина, где выполняется Java код, есть свой планировщик потоков, называемый Thread Scheduler. Именно он решает, какой поток когда запускать. Программист не может взаимодействовать с этим планировщиком напрямую из Java кода, но он может через JVM попросить планировщик на какое-то время поставить поток на паузу, "усыпить" его. Подробнее можно прочитать в статьях "Thread.sleep()" и "How Multithreading works". Более того, можно узнать, как устроены потоки в Windows OS: "Internals of Windows Thread". А теперь увидим это воочию. Сохраним в файл HelloWorldApp.java следующий код: Как видно, у нас есть некоторая задача (task), в которой выполняется ожидание в 60 секунд, после чего завершается программа. Выполняем компиляцию javac HelloWorldApp.java и запуск java HelloWorldApp . Запуск лучше выполнить в отдельном окне. Например, в Windows это будет так: start java HelloWorldApp . При помощи команды jps узнаем PID процесса и откроем список потоков при помощи jvisualvm --openpid pidПроцесса : Как видно, наш поток перешёл в статус Sleeping. На самом деле, сон текущего потока можно сделать более красиво: Вы наверно заметили, что мы везде обрабатываем InterruptedException ? Давайте поймём, зачем.
Прерывание потока или Thread.interrupt
Всё дело в том, что пока поток ожидает во сне, кто-то может захотеть прервать это ожидание. На этот случай мы обрабатываем такое исключение. Сделано это было после того, как метод Thread.stop объявили Deprecated, т.е. устаревшим и нежелательным к использованию. Причиной тому было то, что при вызове метода stop поток просто "убивался", что было очень непредсказуемо. Мы не могли знать, когда поток будет остановлен, не могли гарантировать консистентность данных. Представте, что вы пишете данные в файл и тут поток уничтожают. Поэтому, решили, что логичнее будет поток не убивать, а информировать его о том, что ему следует прерваться. Как на это реагировать — дело самого потока. Более подробно можно прочитать у Oracle в "Why is Thread.stop deprecated?". Посмотрим на пример: В этом примере мы не будем ждать 60 секунд, а сразу напечатаем 'Interrupted'. Всё потому, что мы вызвали у потока метод interrupt . Данный метод выставляет "internal flag called interrupt status". То есть у каждого потока есть внутренний флаг, недоступный напрямую. Но у нас есть native методы для взаимодействия с этим флагом. Но это не единственный способ. Поток может быть в процессе выполнения, не ждать чего-то, а просто выполнять действия. Но может предусмотреть, что его захотят завершить в определённый момент его работы. Например: В примере выше видно, что цикл while будет выполняться до тех пор, пока поток не прервут снаружи. Про флаг isInterrupted важно знать то, что если мы поймали InterruptedException , флаг isInterrupted сбрасывается, и тогда isInterrupted будет возвращать false. Есть также статический метод у класса Thread, который относится только к текущему потоку — Thread.interrupted(), но данный метод сбрасывает значение флага на false! Подробнее можно прочитать в главе "Thread Interruption".
Join — Ожидание завершения другого потока
Понятие Монитор
В многопоточности есть такое понятие, как Monitor. Вообще, слово монитор с латинского переводится как "надзиратель" или "надсмотрщик". В рамках данной статьи попытаемся вспомнить суть, а кто хочет — за подробностями прошу погрузиться в материал из ссылок. Начнём наш путь со спецификации языка Java, то есть с JLS: "17.1. Synchronization". Там сказано следующее: Получается, что для целей синхронизации между потоками Java использует некий механизм, который называется "Монитор". С каждым объектом ассоциирован некоторый монитор, а потоки могут его заблокировать "lock" или разблокировать "unlock". Далее, найдём на сайте Oracle обучающий tutorial: "Intrinsic Locks and Synchronization". В данном туториале говорится, что синхронизация в Java построена вокруг внутренней сущности (internal entity), известной как intrinsic lock или monitor lock. Часто такой лок называют просто "монитор". Также мы опять видим, что каждый объект в Java имеет ассоциированный с ним intrinsic lock. Почитать можно "Java - Intrinsic Locks and Synchronization". Далее важно понять, каким образом объект в Java может быть связан с монитором. У каждого объекта в Java есть заголовок (header) — своего рода внутренние метаданные, которые недоступны программисту из кода, но которые нужны виртуальной машине, чтобы работать с объектами правильно. В состав заголовка объекта входит MarkWord, которое выглядит следующим образом:
Synchronized и ожидание по локу
С понятием монитора, как мы ранее видели, тесно связано понятие "блок синхронизации" (или как ещё называют — критическая секция). Взглянем на пример: Здесь главный поток сначала отправляет задачу task в новый поток, а потом сразу же "захватывает" лок и выполняет с ним долгую операцию (8 секунд). Всё это время task не может для своего выполнения зайти в блок synchronized , т.к. лок уже занят. Если поток не может получить лок, он будет ждать этого у монитора. Как только получит — продолжит выполнение. Когда поток выходит из-под монитора, он освобождает лок. В JVisualVM это будет выглядеть следующим образом: Как видно, статус в JVisualVM называется "Monitor", потому что поток заблокирован и не может занять монитор. В коде тоже можно узнать состояние потока, но название этого состояния не совпадает с терминами JVisualVM, хотя они и схожи. В данном случае th1.getState() в цикле for будет возвращать BLOCKED , т.к. пока выполняется цикл, монитор lock занят main потоком, а поток th1 заблокирован и не может продолжать работу, пока лок не вернут. Кроме блоков синхронизации может быть синхронизирован целый метод. Например, метод из класса HashTable : В одну единицу времени данный метод будет выполняться только одним потоком. Но ведь нам нужен лок? Да, нужен. В случае методов объекта локом будет выступать this . На эту тему есть интересное обсуждение: "Is there an advantage to use a Synchronized Method instead of a Synchronized Block?". Если метод статический, то локом будет не this (т.к. для статического метода не может быть this ), а объект класса (Например, Integer.class ).
Wait и ожидание по монитору. Методы notify и notifyAll
Жизненный цикл потока
- BLOCKED — ожидает захода в защищённую (protected) секцию, т.е. в synchonized блок.
- WAITING — ожидает другой поток по условию. Если условие выполняется, планировщик потоков запускает поток.
LockSupport и парковка потоков
Начиная с Java 1.6 появился интересный механизм, называемый LockSupport. Данный класс ассоциирует с каждым потоком, который его использует, "permit" или разрешение. Вызов метода park возвращается немедленно, если permit доступен, занимая этот самый permit в процессе вызова. Иначе он блокируется. Вызов метода unpark делает permit доступным, если он ещё недоступен. Permit есть всего 1. В Java API для LockSupport ссылаются на некий Semaphore . Давайте посмотрим на простой пример: Данный код будет вечно ждать, потому что в семафоре сейчас 0 permit. А когда в коде вызывается acquire (т.е. запросить разрешение), то поток ожидает, пока разрешение не получит. Так как мы ждём, то обязаны обработать InterruptedException . Интересно, что семафор реализует отдельное состояние потока. Если мы посмотрим в JVisualVM, то увидим, что у нас состояние не Wait, а Park. Посмотрим на ещё один пример: Статус потока будет WAITING, но JVisualVM различает wait от synchronized и park от LockSupport . Почему так важен этот LockSupport ? Обратимся снова к Java API и посмотрим про Thread State WAITING. Как видим, в него можно попасть только тремя способами. 2 способа — это wait и join . А третий — это LockSupport . Локи в Java построены так же на LockSupport и представляют более высокоуровневые инструменты. Давайте попробуем воспользоваться таковым. Посмотрим, например, на ReentrantLock : Как и в прошлых примерах, тут всё просто. lock ожидает, пока кто-то освободит ресурс. Если посмотреть в JVisualVM, мы увидим, что новый поток будет запаркован, пока main поток не отдаст ему лок. Подробнее про локи можно прочитать здесь: "Многопоточное программирование в Java 8. Часть вторая. Синхронизация доступа к изменяемым объектам" и "Java Lock API. Теория и пример использования". Чтобы лучше понять реализацию локов, полезно прочитать про Phazer в обзоре "Класс Phaser". А говоря про различные синхронизаторы, обязательна к прочтению статья на хабре "Справочник по синхронизаторам java.util.concurrent.*".
Относительно часто программы Java добавляют задержку или паузу в свою работу. Это может быть полезно для ускорения выполнения задачи или для приостановки выполнения до завершения другой задачи.
В этом руководстве будут описаны два способа реализации задержек в Java.
2. Подход, Основанный На Потоках
Когда запускается программа Java, она порождает процесс , который выполняется на хост-машине. Этот процесс содержит по крайней мере один поток – основной поток – в котором выполняется программа. Кроме того, Java позволяет многопоточность , что позволяет приложениям создавать новые потоки, которые выполняются параллельно или асинхронно с основным потоком.
2.1. Использование Thread.sleep
Быстрый и грязный способ приостановки в Java-это указать текущему потоку, чтобы он спал в течение определенного периода времени. Это можно сделать с помощью Thread.sleep(миллисекунды) :
Рекомендуется обернуть метод sleep в блок try/catch на случай, если другой поток прерывает спящий поток. В этом случае мы перехватываем исключение InterruptedException и явно прерываем текущий поток, чтобы его можно было перехватить позже и обработать. Это более важно в многопоточной программе, но все же хорошая практика в однопоточной программе на случай, если мы добавим другие потоки позже.
2.2. Использование TimeUnit.sleep
Для лучшей читаемости мы можем использовать TimeUnit.XXX.sleep(y) , Где XXX – единица времени для сна ( СЕКУНДЫ , МИНУТЫ и т. Д.), А y – номер этой единицы для сна. Это использует Thread.sleep за кулисами. Вот пример синтаксиса Единицы времени :
Однако есть некоторые недостатки в использовании этих методов на основе потоков :
- Время сна не совсем точно, особенно при использовании меньших временных приращений, таких как миллисекунды и наносекунды
- При использовании внутри циклов сон будет немного смещаться между итерациями цикла из-за выполнения другого кода, поэтому время выполнения может стать неточным после многих итераций
3. Подход, Основанный На ExecutorService
Java предоставляет интерфейс ScheduledExecutorService, который является более надежным и точным решением. Этот интерфейс может запланировать выполнение кода один раз после указанной задержки или через фиксированные интервалы времени.
Чтобы запустить фрагмент кода один раз после задержки, мы можем использовать метод schedule :
Имя класса :: некоторая задача часть, в которой мы указываем метод, который будет выполняться после задержки:
- некоторая задача – это имя метода, который мы хотим выполнить
- Имя класса – это имя класса, содержащего ту же задачу метод
Для выполнения задачи с фиксированными интервалами времени мы можем использовать метод scheduleAtFixedRate :
Это приведет к многократному вызову метода некоторой задачи , делая паузу для задержки В секундах между каждым вызовом.
Кроме того, позволяя больше вариантов синхронизации, метод ScheduledExecutorService дает более точные временные интервалы, так как он предотвращает проблемы с дрейфом.
4. Заключение
В этой статье мы обсудили два метода создания задержек в программах Java.
Полный код этой статьи можно найти на Github . Это проект на основе Maven, поэтому его должно быть легко импортировать и запускать как есть.
Читайте также: