Как сделать ожидание в java

Добавил пользователь Евгений Кузнецов
Обновлено: 05.10.2024

В частности, у меня есть собственный класс, расширяющий JFrame, и собственный класс, расширяющий JPanel. У меня есть Main.java (в котором есть экземпляр моего класса JFrame), и есть точка, в которой я не хочу продолжать, пока пользователь не нажмет пробел:

Итак, в моем классе фреймов, как мне это реализовать:

Кстати, у меня есть KeyListener на моем JFrame, который работает при нажатии кнопки пробела. Но я не совсем уверен, как включить это в то, что я пытаюсь сделать.

Пример кода был бы потрясающим!

4 ответы

ответ дан 16 авг.

Помните, что графический интерфейс Swing управляется событиями. Так что не ждите ничего. Вместо этого дайте вашему классу какое-то поле состояния, возможно, логическое, измените переменную состояния при нажатии клавиши, возможно, используя привязки клавиш, а затем не разрешайте определенное поведение, если состояние не было изменено (с помощью операторов if).

ответ дан 16 авг.

Не используйте KeyLIstener.

Используйте привязки клавиш. Вам уже была предоставлена ​​ссылка на руководство по привязке клавиш.

ответ дан 16 авг.

Использование Защелка когда вызывается await (), текущий поток main () будет ждать, пока метод countDown () не будет вызван из другого потока.

Однако, если поток, вызывающий waitForSpace (), является EDT, вы заставляете EDT ждать самого себя, вызывая тупик. Насколько мне известно, невозможно дождаться EDT и все еще получать ключевые события.

Это будет работать, пока JFrame имеет фокус:

ответ дан 16 авг.

не могли бы вы похвалить это? я никогда раньше не делал ничего подобного. куда пойдет этот код? - КодГай

регистрируется ли это только один раз при вводе пробела? вроде регистрируется не один раз. - КодГай

каждый раз, когда метод вызывается, к слушателям будет добавляться новый KeyEventDispatcher, но после нажатия пробела он будет удален. - Гарретт Холл

но если я удерживаю пробел, запускаются несколько событий. как мне сделать так, чтобы он основывался на введенном пробеле, а не на нажатом пробеле? - КодГай

Не тот ответ, который вы ищете? Просмотрите другие вопросы с метками java swing jframe jpanel keylistener or задайте свой вопрос.

Перед изучением данной темы рекомендуется ознакомиться со следующей темой:

Содержание

Поиск на других ресурсах:

1. Средства взаимодействия между потоками выполнения. Методы wait() , notify() , notifyAll()

Для обеспечения взаимодействия между потоками выполнения в классе Object реализованы 3 метода.

1. Метод wait() — переводит вызывающий поток в состояние ожидания. В этом случае вызывающий поток освобождает монитор, который имеет доступ к ресурсу. Ожидание продолжается до тех пор, пока другой поток выполнения, который вошел в монитор, не вызовет метод notify() .

Согласно документации Java общая наиболее распространенная реализация метода следующая:

В языке Java метод wait() имеет другие перегружены реализации, которые позволяют ожидать указанный период времени.

2. Метод notify() — возобновляет выполнение потока, из которого был вызван метод wait() для того же объекта.

Согласно документации Java общая форма метода следующая:

3. Метод notifyAll() — возобновляет выполнение всех потоков, из которых был вызван метод wait() для того же объекта. Управление передается одному из этих потоков.

Общая форма метода следующая:

2. Схема (рисунок) взаимодействия между двумя параллельными потоками

На рисунке изображено взаимодействие между двумя потоками, которые выполняются параллельно. Потоки обмениваются экземпляром класса Value, который сохраняет некоторое значение. Каждый поток представлен классом. Класс, формирующий экземпляр Value называется PutValue . Класс, считывающий экземпляр Value называется GetValue .

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

Java. Потоки выполнения. Взаимодействие между параллельными потоками

Рисунок 1. Взаимодействие между параллельными потоками PutValue и GetValue , которые обмениваются общим ресурсом Value

3. Реализация системы опроса между потоками выполнения. Пример обмена значениями между двумя потоками, которые выполняются параллельно

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

Решение. Для решения данной задачи нужно сформировать 3 класса:

  • класс Number , который определяет число используемое для обмена между потоками;
  • класс FormNumber — представляет поток, формирующий случайные числа, которые будут передаваться в поток GetNumber ;
  • класс GetNumber — представляет поток, получающий числа из потока FormNumber .

Результат выполнения программы

4. Пример последовательного синхронизированного использования общего массива чисел между потоками. Реализация системы опроса

Решение данного примера можно использовать в качестве шаблона для реализации взаимодействия между собственно разработанными потоками.

Условие задачи. Реализовать доступ к массиву поочередно из двух потоков. Первый поток формирует массив чисел указанной длины. Второй поток считывает этот массив чисел. Второй поток ожидает, когда произойдет формирование массива чисел первым потоком. Точно также первый поток должен ожидать до тех пор, пока второй поток не завершит чтение массива.

Продемонстрировать взаимодействие потоков выполнения указанное количество раз.

Решение. Для решения задачи нужно реализовать 3 класса:

  • Array — определяет массив целых чисел заданного размера;
  • FormArray — предназначен для формирования массива случайных чисел произвольного размера;
  • GetArray — используется для получения массива чисел, предварительно сгенерированных в классе FormArray .

В классе Array реализованы следующие члены класса:

  • AI — массив целых чисел. Этот массив является общим ресурсом для потоков, которым соответствуют классы FormArray и GetArray ;
  • formArray — флажок типа boolean , который используется для определения потоками периода ожидания;
  • конструктор Array() , который получает параметром размер массива. В конструкторе выделяется память для массива AI ;
  • метод get() — используется потоком GetArray для считывания массива AI ;
  • метод set() — используется потоком FormArray для формирования массива целых чисел;
  • метод getArray() — ссылка на скрытый ( private ) массив AI .

Оба класса FormArray и GetArray реализуют интерфейс Runnable . Классы инкапсулируют потоки выполнения. В классах реализованы следующие составляющие элементы:

Каждый поток ассоциирован с классом java . lang . Thread . Есть два основных способа использования объектов Thread в многопоточном программировании:

  • Прямое создание и управление потоками с помощью создания экземпляров класса Thread .
  • Абстрагирование от управления потоками и передача задач в executor.

Содержание

java.lang.Thread

Объявление и запуск потока

Приложение, создающее экземпляр Thread , должно предоставить код, который будет выполняться в отдельном потоке. Есть два способа сделать это:

  • Предоставить экземпляр класса, реализующего интерфейс java . lang . Runnable . Этот класс имеет один метод run ( ) , который должен содержать код, который будет выполняться в отдельном потоке. Экземпляр класса java . lang . Runnable передаётся в конструктор класса Thread вот так:
  • Написать подкласс класса Thread . Класс Thread сам реализует интерфейс java . lang . Runnable , но его метод run ( ) ничего не делает. Приложение может унаследовать класс от Thread и переопределить метод run ( ) :

Обратите внимание, что оба примера вызывают метод Thread . start ( ) для запуска нового потока. Именно он запускает отдельный поток. Если просто вызывать метод run ( ) , то код будет выполняться в том же потоке, отдельный поток создаваться не будет.

Какой способ использовать? Первый способ, где предоставляется экземпляр класса, реализующего Runnable , более общий, так как в этом случае класс может наследоваться от отличного от Thread класса. Второй способ проще использовать в простых приложениях, но он ограничен тем, что ваш класс будет наследником Thread .

Приостанавливаем исполнение с помощью метода sleep

Метод sleep класса Thread останавливает выполнение текущего потока на указанное время. Он используется, когда нужно освободить процессор, чтобы он занялся другими потоками или процессами, либо для задания интервала между какими-нибудь действиями.

Есть два варианта метода sleep : первый принимает в качестве параметра количество миллисекунд, на которое нужно остановить текущий поток, второй дополнительно принимает второй параметр, в котором указывается количество наносекунд, на которые нужно дополнительно остановить поток.

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

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

Прерывание потока

Прерывание (interrupt) — это сигнал для потока, что он должен прекратить делать то, что он делает сейчас, и делать что-то другое. Что должен делать поток в ответ на прерывание, решает программист, но обычно поток завершается.

Поток отправляет прерывание вызывая метод public void interrupt ( ) класса Thread . Для того чтобы механизм прерывания работал корректно, прерываемый поток должен поддерживать возможность прерывания своей работы.

Как поток должен поддерживать прерывание своей работы? Это зависит от того, что он сейчас делает. Если поток часто вызывает методы, которые могут бросить InterruptedException , то он просто вызывает return при перехвате подобного исключения. Пример:

Многие методы, которые бросают InterruptedException , например методы sleep , останавливают своё выполнение и возвращают управление в вызвавший их код при получении прерывания (interrupt).

Что если поток выполняется длительное время без вызова методов, которые бросают исключение InterruptedException ? Тогда он может периодически вызывать метод Thread . interrupted ( ) , который возвращает true , если получен сигнал о прерывании. Например:

В этом примере код просто проверяет на наличие сигнала о прерывании, и выходит из потока, если сигнал есть. В более сложных приложениях имеет смысл бросить исключение InterruptedException :

Это позволяет располагать код обработки прерывания потока в одной клаузе catch .

Механизм прерывания реализован с помощью внутреннего флага, известного как статус прерывания (interrupt status). Вызов Thread . interrupt ( ) устанавливает этот флаг. Когда поток проверяет наличие прерывания вызовов Thread . interrupted ( ) , то флаг статуса прерывания сбрасывается. Нестатический метод isInterrupted ( ) , который используется одним потоком для проверки статуса прерывания другого потока, не меняет флаг статуса прерывания.

По соглашению любой метод, который прерывает свою выполнение бросая исключение InterruptedException , очищает флаг статуса прерывания, когда он бросает это исключение. Однако есть вероятность, что флаг статуса прерывания будет сразу же установлен ещё раз, если другой поток вызовет interrupt ( ) .

Соединение

Метод join позволяет одному потоку ждать завершения другого потока. Если t является экземпляром класса Thread , чей поток в данный момент продолжает выполняться, то

приведёт к приостановке выполнения текущего потока до тех пор, пока поток t не завершит свою работу. Метод join ( ) имеет варианты с параметрами:

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

Как и методы sleep , методы join отвечают на сигнал прерывания, останавливая процесс ожидания и бросая исключение InterruptedException .

Простой пример

Пример состоит из двух потоков. Первый поток является главным потоком приложения, который имеет каждая программа на Java. Главный поток создаёт новый поток и ждёт его завершения. Если второй поток выполняется слишком долго, то главный поток прерывает его.

Синхронизация

Потоки общаются в основном разделяя свои поля и поля объектов между собой. Эта форма общения очень эффективна, но делает возможным два типа ошибок: вмешательство в поток (thread interference) и ошибки консистентности памяти (memory consistency errors). Для того чтобы предотвратить эти ошибки, нужно использовать синхронизацию потоков.

Вмешательство в поток (thread interference)

Рассмотрим простой класс Counter:

Counter спроектирован так, что каждый вызов метода increment добавляет 1 к c , а каждый вызов decrement вычитает 1 из c . Однако если объект Counter используется несколькими потоками, то вмешательство в поток может помешать этому коду работать как ожидалось.

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

Может показаться, что операции над экземплярами Counter не могут перекрываться, так как все операции над c являются одиночными простыми инструкциями. Однако даже простые инструкции могут транслироваться виртуальной машиной в несколько шагов. Выражение c ++ может быть разложено на три шага:

  1. Получить текущее значение c .
  2. Увеличить полученное значение на 1.
  3. Сохранить увеличенное значение в c .

Предположим, что поток A вызывает increment , и в то же самое время поток B вызывает decrement . Начальное значение c равно 0, их пересечённые действия могут породить следующую последовательность шагов:

  1. Поток A получает c .
  2. Поток B получает c .
  3. Поток A увеличивает полученное значение, в результате получает 1.
  4. Поток B уменьшает полученное значение, в результате получает -1.
  5. Поток A сохраняет результат 1 в c .
  6. Поток B сохраняет результат -1 в c .

Результат потока A потерян, он был перезаписан потоком B. Такое частичное перекрытие действий — это только одна из возможностей. В некоторых других ситуациях может оказаться, что результат потока B будет потерян, либо ошибок не будет совсем. Из-за этого ошибки вмешательства в поток трудно обнаруживать и исправлять.

Ошибки консистентности памяти (memory consistency errors)

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

Ключ к исключению ошибок консистентности памяти — это пониманию связи происходит-до (happens-before). Эта связь гарантирует, что данные, записанные в память одной инструкцией, видимы в другой. Рассмотрим следующий пример. Предположим, что поле типа int объявлено и инициализировано:

Поле counter используется совместно двумя потоками A и B. Предположим, что поток A увеличивает counter :

сразу же после этого поток B выводит в консоль значение counter :

Если бы обе инструкции были выполнены одним потоком, то можно было бы смело предположить, что в консоль выведется число 1. Но если две инструкции выполняются разными потоками, то может быть выведено 0, так как нет гарантии, что изменение counter потоком A будет видимо потоком B, до тех пор пока программист не обеспечит связь происходит-до (happens-before) между этими инструкциями.

Есть разные способы создания связи происходит-до (happens-before). Один из них — это синхронизация, она будет расписана в следующих пунктах.

Мы уже видели два действия, которые порождают связь происходит-до (happens-before):

  • Когда инструкция вызывает Thread . start , каждая инструкция, которая имеет связь происходит-до (happens-before) с этой инструкцией, также имеет связь происходит-до (happens-before) с каждой инструкцией, выполняемой новым потоком. Все последствия действий кода, который был выполнен до создания нового потока, видимы новым потоком.
  • Когда поток завершается и приводит Thread . join другого потока к возврату выполнения, то все инструкции, которые были выполнены завершённым потоком, имеют связь происходит-до (happens-before) со всеми инструкциями, которые следуют за успешным соединением потока. Все последствия действий кода в потоке теперь видимы потоком, который осуществил соединение.

Синхронизированные (synchronized) методы

Язык программирования Java предоставляет два базовых способа синхронизации: синхронизированные методы (synchronized methods) и синхронизированные инструкции (synchronized statements). Есть другие, более сложные, способы синхронизации, они будут рассмотрены в дальнейшем.

Чтобы сделать метод синхронизированным (synchronized), просто добавьте ключевое слово synchronized к его объявлению:

Если count является экземпляром класса SynchronizedCounter , то синхронизированные методы имеют два эффекта:

  • Во-первых, два вызова синхронизированных метода на одном и том же объекте не могут пересекаться. Когда один поток выполняет синхронизированный метод объекта, то другие потоки, которые вызывают синхронизированные методы того же самого объекта, блокируются (приостанавливают своё выполнение) до тех пор, пока первый поток не завершит работу с объектом.
  • Во-вторых, когда синхронизированный метод завершает своё выполнение, то он автоматически делает связь происходит-до (happens-before) со всеми последующими вызовами синхронизированных методов того же самого объекта. Это гарантирует, что изменения состояния объекта будут видимы для других потоков.

Заметьте, что конструкторы не могут быть синхронизированными. Использование ключевого слова synchronized для конструктора приведёт к ошибке компиляции. Синхронизированные конструкторы не имеют смысла, так как только один поток, который создаёт объект, должен иметь доступ к нему во время создания.

Но тогда другие потоки смогут использовать instances для получения доступа к объекту до того, как его создание будет завершено.

Синхронизированные методы — простая стратегия для предотвращения вмешательства в поток (thread interference) и ошибок консистентности памяти (memory consistency errors): Если объект видим более чем одному потоку, то все чтения и записи полей объекта должны происходить через синхронизированные методы. (Важное исключение: поля с модификатором final , которые не могут быть изменены после создания экземпляра объекта, могут безопасно читаться из несинхронизированных методов после создания конструктора) Эта стратегия эффективна, но может содержать проблемы с живучестью (liveness).

Внутренние блокировки и синхронизация

Синхронизация построена вокруг внутренней сущности, известной как внутренняя блокировка или блокировка монитора. Внутренняя блокировка играет роль в обоих аспектах синхронизации: обеспечивает эксклюзивный доступ к внутреннему состоянию объекта и обеспечивает связь происходит-до (happens-before).

Каждый объект имеет внутреннюю блокировку (монитор), который с ним связан. По соглашению поток, которому требуется эксклюзивный и согласованный доступ к полям объекта, должен получить внутреннюю блокировку объекта перед доступом к ним и освободить внутреннюю блокировку объекта после совершения необходимых действий с ними. Поток владеет блокировкой объекта между временем получения и временем освобождения блокировки. Пока поток держит внутреннюю блокировку (внутренний монитор) никакой другой объект не может получить ту же самую блокировку. Другой поток будет блокирован (приостановлен) при попытке получить эту блокировку.

Когда поток вызывает синхронизированный метод, то он автоматически получает внутреннюю блокировку этого объекта и освобождает её по завершении метода. Освобождение блокировки происходит даже при возникновении неперехваченного исключения.

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

В отличие от синхронизированных методов синхронизированные инструкции должны указать объект, который предоставляет внутреннюю блокировку:

Каждый объект, помимо связанного монитора, имеет связанный набор ожиданий (wait set). Набор ожиданий - это набор потоков.

Когда объект создается впервые, его набор ожидания пуст. Элементарные действия, которые добавляют потоки и удаляют потоки из наборов ожидания, являются атомарными. Наборы ожидания управляются исключительно с помощью методов Object.wait, Object.notify и Object.notifyAll.

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

17.2.1. Ожидание (wait)

Действия ожидания происходят при вызове метода wait() или форм wait(long millisecs) и wait(long millisecs, int nanosecs).

Вызов wait(long millisecs) с нулевым параметром или вызов wait(long millisecs, int nanosecs) с двумя нулевыми параметрами эквивалентен вызову wait().

Поток нормально возвращается из ожидания, если он возвращается без исключения InterruptedException.

Пусть поток t будет потоком, выполняющим метод wait для объекта m, и пусть n будет количеством действий блокировки t на m, которые не были сопоставлены действиями разблокировки. Происходит одно из следующих действий:

  • Если n равно нулю (т. е. поток t еще не имеет блокировки для цели m), то генерируется исключение IllegalMonitorStateException.
  • Если это wait с временем и аргумент nanosecs не находится в диапазоне 0-999999 или аргумент millisecs отрицательный, возникает исключение IllegalArgumentException.
  • Если поток t прерывается, возникает исключение InterruptedException, а статус прерывания t устанавливается в значение false.
  • В противном случае имеет место следующая последовательность:
  • Поток t добавляется в набор ожидания объекта m и выполняет n действий разблокировки на m.

Поток t не выполняет никаких дальнейших инструкций, пока он не будет удален из набора ожидания m. Поток может быть удален из набора ожидания из-за любого из следующих действий и возобновится через некоторое время:

  • Действие уведомления (notify), выполняемое на m, в котором t выбрано для удаления из набора ожидания.
  • Действие notifyAll, выполняемое на m.
  • Прерывание (interrupt) выполняется на t.
  • Если это wait с временем ожидания, внутреннее действие, удаляющее t из набора ожидания m, которое происходит по истечении, по крайней мере, millisecs миллисекунд плюс nanosecs наносекунд с начала этого действия ожидания.

Внутреннее действие реализации. Реализациям разрешено, хотя и не рекомендуется, выполнять "ложные пробуждения", то есть удалять потоки из наборов ожидания и, таким образом, разрешать возобновление без явных инструкций для этого.

Обратите внимание, что это положение требует практики кодирования Java с использованием wait только внутри циклов, которые завершаются только тогда, когда выполняется какое-то логическое условие, которого ожидает поток.

Каждый поток должен определять порядок событий, которые могут привести к его удалению из набора ожидания. Этот порядок не обязательно должен соответствовать другим порядкам, но поток должен вести себя так, как если бы эти события произошли в этом порядке.

Например, если поток t находится в наборе ожидания для m, а затем происходит как прерывание t, так и уведомление m, должен быть порядок этих событий. Если считается, что прерывание произошло первым, тогда t в конечном итоге вернется из ожидания, выбрасывая InterruptedException, и какой-то другой поток в ожидании, установленном для m (если таковой существует на момент уведомления), должен получить уведомление. Если считается, что уведомление появилось первым, то в конечном итоге t вернется в обычном режиме из ожидания с ожидающим прерыванием.

У меня есть поток, который время от времени обновляет его состояние, и я хочу, чтобы второй поток мог ждать завершения первого потока. Что-то вроде этого:

Теперь это выглядит хорошо, и все, если только notifyAll() потока 1 не запускается до wait() потока 2, и в этом случае поток 2 ожидает, пока поток 1 не уведомит снова (что может никогда не произойти).

Мои возможные решения:

а) Я мог бы использовать CountDownLatch или Future, но у обоих есть проблема в том, что они по сути запускаются только один раз. То есть в цикле while потока 1 мне нужно было бы создать новый foo для ожидания каждый раз, а поток 2 должен был бы спросить, какого foo ждать. У меня плохое предчувствие просто писать

так как я боюсь, что при foo = new FutureTask(), что происходит, когда кто-то ждал старого foo (по "какой-то причине", set не вызывался, например, ошибка в обработке исключений)?

б) Или я мог бы использовать семафор:

Но я боюсь, что есть некоторое состояние гонки, если несколько потоков ждут его, пока еще один сигнал () и сброс ().

Вопрос:

Нет ли в Java API ничего похожего на поведение Windows Event? Или, если вы презираете Windows, что-то вроде WaitGroup golang (то есть CountDownLatch, который позволяет countUp())? Что-нибудь?

Как это сделать вручную:

Поток 2 не может просто ждать из-за ложного пробуждения, и в Java нет способа узнать, почему возвращен Object.wait(). Поэтому мне нужна переменная условия, которая хранит, сигнализировано ли событие или нет. Тема 2:

И Поток 1, конечно, устанавливает условие в true в синхронизированном блоке. Спасибо Weekens за подсказку!

Существует ли существующий класс, который переносит это поведение?

Читайте также: