Тема 32. Конкуренция - BelyiZ/JavaCourses GitHub Wiki

Содержание:

  1. Блокировка потоков
    1. Метод wait и его модификации
      1. Метод wait с аргументом timeout
      2. Метод wait с аргументами timeout и nanos
      3. Метод wait без аргументов
    2. Методы notify, notifyAll
      1. Метод notify
      2. Метод notifyAll
  2. Список литературы/курсов

Блокировка потоков

В Java есть встроенный механизм управления доступом к общим ресурсам (объектам) из разных потоков. Любой поток может объявить какой-нибудь объект занятым, и другие будут вынуждены ждать, пока занятый объект не освободится.

Пример:

public void print() {
    Object monitor = getMonitor();
    synchronized(monitor){
        System.out.println("text");
    }
}

Если два потока одновременно вызовут метод println(), то первый из них, который войдет в блок, помеченный synchronized заблокирует monitor, а второй поток будет ждать, пока монитор не освободится. Как только поток входит в synchronized-блок, объект-монитор помечается как занятый, и другие потоки будут вынуждены ждать его освобождения. Один и тот же монитор может использоваться в различных частях программы. Монитором принято называть объект, который хранит состояние занят/свободен.

Методов как таковых всего два: wait() и notify(). Остальные – это лишь их модификации.

Иногда в приложении может оказаться такая ситуация, что поток вошел в блок synchronized, заблокировал монитор и не может работать дальше, так как еще не хватает данных: например, файл который она должна обработать еще не загрузился. Возможно просто подождать, когда файл скачается, либо проверять в цикле – если файл еще не скачался – спать, например, секунду и опять проверять и так далее.

Пример:

while(!file.isDownloaded()) {
    Thread.sleep(1000);
}
processFile(file);

Такое ожидание приводит к потере времени и оптимальности процесса. Один поток заблокировал монитор, а другие вынуждены тоже ждать, хотя их данные для работы могут быть уже готовы. Для решения этой проблемы и был придуман метод wait(). Вызов этого метода приводит к тому, что поток освобождает монитор и «встает на паузу».

Метод wait и его модификации

Метод wait() можно вызвать у объекта-монитора и только тогда, когда это монитор занят – то есть внутри блока synchronized. При этом поток временно прекращает работу, а монитор освобождается, чтобы им могли воспользоваться другие потоки.

Часто встречаются ситуации, когда в блок synchronized зашел поток, вызвал там wait(), освободил монитор. Затем туда вошел второй поток и тоже встал на паузу, затем третий и так далее.

Существуют модификации метода wait():

void wait(long timeout) - поток ждет, но через переданное количество миллисекунд автоматически возвращается к выполнению. void wait(long timeout, int nanos) - поток ждет, но через переданное количество миллисекунд и наносекунд автоматически возвращается к выполнению.

Метод wait с аргументом timeout

public final void wait(long timeout) throws InterruptedException - заставляет текущий поток ждать, пока другой поток не вызовет метод notify() или метод notifyAll() для этого объекта, или пока не истечет указанное количество времени.

Параметры:

  • timeout - максимальное время ожидания в миллисекундах.

Выбрасываемые исключения:

  • IllegalArgumentException - если значение timeout является отрицательным.
  • IllegalMonitorStateException - если текущий поток не является владельцем монитора объекта.
  • InterruptedException - если какой-либо поток прерывал текущий поток до или в то время, когда текущий поток ожидал уведомления. Прерванное состояние текущего потока очищается при возникновении этого исключения.

Для того чтобы снять поток с "паузы" существует метод notify()

Текущий поток должен владеть монитором этого объекта.

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

  1. Другой поток вызывает метод notify() для этого объекта, и поток T случайно выбирается в качестве потока, который нужно пробудить.
  2. Другой поток вызывает метод notifyAll() для этого объекта.
  3. Другой поток прерывает поток Т.
  4. Указанное количество реального времени истекло. Однако, если timeout равен нулю, то реальное время не учитывается, и поток просто ждет, пока не получит уведомление.

Если хотя бы одно условие выполняется, поток T удаляется из списка ожидания для этого объекта и возобновляет свое выполнение. Затем он конкурирует обычным образом с другими потоками за право синхронизации на объекте; как только он получил контроль над объектом, все его заявки на синхронизацию с объектом восстанавливаются до состояния на момент вызова метода wait().

Поток также может возобновлять выполнение без уведомления, прерывания или истечения тайм-аута, так называемое ложное пробуждение (spurious wakeup). Хотя это редко случается на практике, приложения должны защищаться от него, проверяя условие, которое должно было вызвать пробуждение потока, и продолжая ждать, если условие не выполняется. Другими словами, ожидания всегда должны происходить в циклах, например:

synchronized (obj) {
    while (/*условие, при котором нужно продолжать ждать*/) {
        obj.wait(timeout);... // Выполняем действие подходящее для условия
    }
}

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

Метод wait с аргументами timeout и nanos

public final void wait(long timeout, int nanos) throws InterruptedException - заставляет текущий поток ждать, пока другой поток не вызовет метод notify() или метод notifyAll() для этого объекта, или какой-либо другой поток прервет текущий поток, или пока не истекло определенное количество реального времени. Этот метод аналогичен методу wait(long) с одним аргументом, но он позволяет лучше контролировать время ожидания уведомления перед тем, как возобновить работу.

Параметры:

  • timeout - максимальное время ожидания в миллисекундах.
  • nanos - дополнительное время в диапазоне наносекунд 0-999999.

Выбрасываемые исключения:

  • IllegalArgumentException - если значение timeout является отрицательным или значение nanos не находится в диапазоне 0-999999.
  • IllegalMonitorStateException - если текущий поток не является владельцем монитора этого объекта.
  • InterruptedException - если какой-либо поток прерывал текущий поток до или в то время, когда текущий поток ожидал уведомления. Прерванное состояние текущего потока очищается при возникновении этого исключения.

Во всех других отношениях этот метод делает то же самое, что и метод wait(long) с одним аргументом. В частности, wait(0, 0) означает то же самое, что и wait(0).

Текущий поток должен владеть монитором этого объекта. Поток освобождает владельца этого монитора и ожидает, пока не произойдет одно из следующих условий:

Другой поток уведомляет потоки, ожидающие на мониторе этого объекта, чтобы они проснулись либо посредством вызова метода notify(), либо метода notifyAll(). Истек срок ожидания, указанный аргументами: 1000000 * timeout + nanos наносекунд.

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

synchronized (obj) {
    while (/*условие, при котором нужно продолжать ждать*/) {
        obj.wait(timeout,nanos);
        ... // Выполняем действие подходящее для условия
    }
}

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

Метод wait без аргументов

public final void wait() throws InterruptedException - заставляет текущий поток ждать, пока другой поток не вызовет метод notify() или метод notifyAll() для этого объекта. Другими словами, этот метод ведет себя точно так же, как если бы он просто выполнял вызов wait(0).

Выбрасываемые исключения:

  • IllegalMonitorStateException - если текущий поток не является владельцем монитора этого объекта.
  • InterruptedException - если какой-либо поток прерывал текущий поток до или в то время, когда текущий поток ожидал уведомления. Прерванное состояние текущего потока очищается при возникновении этого исключения.

Текущий поток должен владеть монитором этого объекта. Поток освобождает владельца этого монитора и ожидает, пока другой поток не уведомит потоки, ожидающие на мониторе этого объекта, чтобы он проснулся либо посредством вызова метода notify(), либо метода notifyAll(). Затем поток ожидает, пока не получит право собственности на монитор, и возобновит выполнение.

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

synchronized (obj) {
    while (/*условие, при котором нужно продолжать ждать*/){
        obj.wait();... // Выполняем действие подходящее для условия
    }
}

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

Методы notify, notifyAll

Для того чтобы снять поток с "паузы" существует метод notify().

Методы notify()/notifyAll() можно вызвать у объекта-монитора, когда этот монитор занят – т.е. внутри блока synchronized. Метод notifyAll() снимает с паузы все потоки, которые стали на паузу с помощью данного объекта-монитора.

Метод notify() возобновляет один случайный поток из списка ожидания объекта-монитора, на котором вызван этот метод, метод notifyAll() – возобновляет выполнение всех потоков монитора.

Метод notify

public final void notify() - пробуждает один поток, который ожидает на мониторе этого объекта. Если таких потоков несколько для пробуждения выбирается один из них. Выбор является случайным и происходит на усмотрение реализации.

Выбрасывает исключения:

  • IllegalMonitorStateException - если текущий поток не является владельцем монитора этого объекта.

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

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

Поток становится владельцем монитора объекта одним из трех способов:

  1. Выполняя synchronized метод этого объекта.
  2. Выполняя тело synchronized-блока, который синхронизируется на объекте.
  3. Выполняя synchronized static метод (для объектов типа Class).

Только один поток одновременно может владеть монитором объекта.

Метод notifyAll

public final void notifyAll() - пробуждает все потоки, из списка ожидания на мониторе этого объекта.

Выбрасывает исключения:

  • IllegalMonitorStateException - если текущий поток не является владельцем монитора этого объекта.

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

Список литературы/курсов

Тема 31. Введение в многопоточность | Оглавление | Тема 33. Работа с потоками ForkJoinPool