Добрый день! Начал проходить курс "Программирование на Java". Как я понимаю,курс создавался приблизительно в 2015 году. Не потерял ли данный курс свою актуальность? Стоит ли проходить его в 2023 году, или же лучше найти что-то более новое? |
Потоки выполнения. Синхронизация
Методы wait(), notify(), notifyAll() класса Object
Наконец, перейдем к рассмотрению трех методов класса Object, завершая описание механизмов поддержки многопоточности в Java.
Каждый объект в Java имеет не только блокировку для synchronized блоков и методов, но и так называемый wait-set, набор потоков исполнения. Любой поток может вызвать метод wait() любого объекта и таким образом попасть в его wait-set. При этом выполнение такого потока приостанавливается до тех пор, пока другой поток не вызовет у этого же объекта метод notifyAll(), который пробуждает все потоки из wait-set. Метод notify() пробуждает один случайно выбранный поток из данного набора.
Однако применение этих методов связано с одним важным ограничением. Любой из них может быть вызван потоком у объекта только после установления блокировки на этот объект. То есть либо внутри synchronized -блока с ссылкой на этот объект в качестве аргумента, либо обращения к методам должны быть в синхронизированных методах класса самого объекта. Рассмотрим пример:
public class WaitThread implements Runnable { private Object shared; public WaitThread(Object o) { shared=o; } public void run() { synchronized (shared) { try { shared.wait(); } catch (InterruptedException e) {} System.out.println("after wait"); } } public static void main(String s[]) { Object o = new Object(); WaitThread w = new WaitThread(o); new Thread(w).start(); try { Thread.sleep(100); } catch (InterruptedException e) {} System.out.println("before notify"); synchronized (o) { o.notifyAll(); } } }
Результатом программы будет:
before notify after wait
Обратите внимание, что метод wait(), как и sleep(), требует обработки InterruptedException, то есть его выполнение также можно прервать методом interrupt().
В заключение рассмотрим более сложный пример для трех потоков:
public class ThreadTest implements Runnable { final static private Object shared=new Object(); private int type; public ThreadTest(int i) { type=i; } public void run() { if (type==1 || type==2) { synchronized (shared) { try { shared.wait(); } catch (InterruptedException e) {} System.out.println("Thread "+type+" after wait()"); } } else { synchronized (shared) { shared.notifyAll(); System.out.println("Thread "+type+" after notifyAll()"); } } } public static void main(String s[]) { ThreadTest w1 = new ThreadTest(1); new Thread(w1).start(); try { Thread.sleep(100); } catch (InterruptedException e) {} ThreadTest w2 = new ThreadTest(2); new Thread(w2).start(); try { Thread.sleep(100); } catch (InterruptedException e) {} ThreadTest w3 = new ThreadTest(3); new Thread(w3).start(); } }Пример 12.5.
Результатом работы программы будет:
Thread 3 after notifyAll() Thread 1 after wait() Thread 2 after wait()Пример 12.6.
Рассмотрим, что произошло. Во-первых, был запущен поток 1, который тут же вызвал метод wait() и приостановил свое выполнение. Затем то же самое произошло с потоком 2. Далее начинает выполняться поток 3.
Сразу обращает на себя внимание следующий факт. Еще поток 1 вошел в synchronized -блок, а стало быть, установил блокировку на объект shared. Но, судя по результатам, это не помешало и потоку 2 зайти в synchronized -блок, а затем и потоку 3. Причем, для последнего это просто необходимо, иначе как можно "разбудить" потоки 1 и 2?
Можно сделать вывод, что потоки, прежде чем приостановить выполнение после вызова метода wait(), отпускают все занятые блокировки. Итак, вызывается метод notifyAll(). Как уже было сказано, все потоки из wait-set возобновляют свою работу. Однако чтобы корректно продолжить исполнение, необходимо вернуть блокировку на объект, ведь следующая команда также находится внутри synchronized -блока!
Получается, что даже после вызова notifyAll() все потоки не могут сразу возобновить работу. Лишь один из них сможет вернуть себе блокировку и продолжить работу. Когда он покинет свой synchronized -блок и отпустит объект, второй поток возобновит свою работу, и так далее. Если по какой-то причине объект так и не будет освобожден, поток так никогда и не выйдет из метода wait(), даже если будет вызван метод notifyAll(). В рассмотренном примере потоки один за другим смогли возобновить свою работу.
Кроме того, определен метод wait() с параметром, который задает период тайм-аута, по истечении которого поток сам попытается возобновить свою работу. Но начать ему придется все равно с повторного получения блокировки.
Заключение
В этой лекции были рассмотрены принципы построения многопоточного приложения. В начале разбирались достоинства и недостатки такой архитектуры – как правило ОС не выделяет отдельный процессор под каждый процесс, а значит применяется процедура time slicing. Было выделено три признака, указывающие на целесообразность запуска нескольких потоков в рамках программы.
Основу работы с потоками в Java составляют интерфейс Runnable и класс Thread. С их помощью можно запускать и останавливать потоки, менять их свойства, среди которых основные: приоритет и свойство daemon. Главная проблема, возникающая в таких программах - одновременный доступ нескольких потоков к одним и тем же данным, в первую очередь -– к полям объектов. Для понимания, как в Java решается эта задача, был сделан краткий обзор по организации памяти в JVM, работы с переменными и блокировками. Блокировки, несмотря на название, сами по себе не ограничивают доступ к переменной. Программист использует их через ключевое слово synchronized, которое может быть указано в сигнатуре метода или в начале блока. В результате выполнение не будет продолжено, пока блокировка не освободится.
Новый механизм порождает новую проблему - взаимные блокировки (deadlock), к которой программист всегда должен быть готов, тем более, что Java не имеет встроенных средств для определения такой ситуации. В лекции разбирался пример, как организовать работу программы без "зависания" ожидающих потоков.
В завершение рассматривались специализированные методы базового класса Object, которые также позволяют управлять последовательностью работы потоков.