Java: руководство для начинающих
Шрифт:
Выполнение этой версии программы дает такой же правильный результат, как и предыдущей ее версии, в которой использовался синхронизированный метод. Организация взаимодействия потоков с помощью методов notify , wait и notifyAll
Рассмотрим для примера следующую ситуацию. В потоке Т выполняется синхронизированный метод, которому необходим доступ к ресурсу R. Этот ресурс временно недоступен. Что должен предпринять поток т? Если он будет ожидать в цикле освобождения ресурса R, объект будет по-прежнему заблокирован и другие потоки не смогут обратиться к нему. Такое решение малопригодно, поскольку оно сводит на нет все преимущества программирования в многопоточной среде. Намного лучше, если поток Т временно разблокирует объект и позволит другим потокам воспользоваться его методами. Когда ресурс R станет доступным, поток т получит об этом уведомление и возобновит свое исполнение. Но для того чтобы такое решение можно было реализовать, необходимы
Эти методы реализованы в классе Object, поэтому они доступны для любого объекта. Но обратиться к ним можно только из синхронизированного контекста. А применяются они следующим образом. Когда поток временно приостанавливает свое исполнение, он вызывает метод wait . При этом поток переходит в состояние ожидания и монитор данного объекта освобождается, позволяя другим потокам использовать объект. Впоследствии ожидающий поток возобновит свое выполнение, когда другой поток войдет в тот же самый монитор и вызовет метод notify или notifyAll .
В классе Object определены различные формы объявления метода wait , как показано ниже. final void wait throws InterruptedException final void wait(long миллисекунд) throws InterruptedException final void wait(long миллисекунд, int наносекунд) throws InterruptedException
В первой своей форме метод wait переводит поток в режим ожидания до поступления уведомления. Во второй форме метода организуется ожидание уведомления или до тех пор, пока не истечет указанный период времени. А третья форма позволяет точнее задавать период времени в наносекундах.
Ниже приведены общие формы объявления методов notify и notifyAll . final void notifyO final void notifyAll
При вызове метода notify возобновляется исполнение одного ожидающего потока. А метод notifyAll уведомляет все потоки об освобождении объекта, и тот поток, который имеет наивысший приоритет, получает доступ к объекту.
Прежде чем рассматривать конкретный пример, демонстрирующий применение метода wait , необходимо сделать важное замечание. Несмотря на то что метод wait должен переводить поток в состояние ожидания до тех пор, пока не будет вызван метод notify или notifyAll , иногда поток выводится из состояния ожидания вследствие так называемой ложной активизации. Условия для ложной активизации сложны, возникают редко, а их обсуждение выходит за рамки этой книги. Но в компании Oracle рекомендуют учитывать вероятность проявления ложной активизации и помещать вызов метода wait в цикл. В этом цикле должно проверяться условие, по которому поток переводится в состояние ожидания. Именно такой подход и применяется в рассматриваемом ниже примере. Пример применения методов wait и notify
Для того чтобы стала понятнее потребность в применении методов wait и notify в многопоточном программировании, рассмотрим пример программы, имитирующей работу часов и выводящей на экран слова "Tick" (Тик) и "Тоск" (Так). Для этой цели создадим класс TickTock, который будет содержать два метода: tick и tock . Метод tick выводит слово "Tick", а метод tock — слово "Тоск". При запуске программы, имитирующей часы, создаются два потока: в одном из них вызывается метод tick , а в другом — метод tock . В результате взаимодействия двух потоков на экран будет выводиться набор повторяющихся сообщений "Tick Tock", т.е. после слова "Tick", обозначающего один такт, должно следовать слово "Тоск", обозначающее другой такт часов. // Применение методов wait и notifyO для имитации часов, class TickTock { String state; // содержит сведения о состоянии часов synchronized void tick(boolean running) { if (!running) { // остановить часы state = "ticked"; notifyO; // уведомить ожидающие потоки return; } System.out.print("Tick "); state = "ticked"; // установить текущее состояние после такта "тик" notify; // Метод tick уведомляет метод tock // о возможности продолжить выполнение. try { while(!state.equals("tocked") ) wait;// Метод tick ожидает завершения метода tock. } catch(InterruptedException exc) { System.out.println("Thread interrupted."); } } synchronized void tock(boolean running) { if(!running) { // остановить часы state = "tocked"; notifyO; // уведомить ожидающие потоки return; } System.out.println("Tock"); state = "tocked"; // установить текущее состояние после такта "так" notifyO; // Метод tock уведомляет метод tick // возможности продолжить выполнение. try { while(!state.equals("ticked") ) wait; // Метод tock ожидает завершения метода tick. } catch(InterruptedException exc) { System.out.println("Thread interrupted."); } } } class MyThread implements Runnable { Thread thrd; TickTock ttOb; // построить новый поток MyThread.(String name, TickTock tt) { thrd = new Thread(this, name); ttOb = tt; thrd.start; // начать поток } // начать исполнение нового потока public void run { if(thrd.getName.compareTo("Tick") == 0) { for(int i=0; i<5; i++) ttOb.tick(true); ttOb.tick(false); } else { for(int i=0; i<5; i++) ttOb.tock(true); ttOb.tock(false); } } } class ThreadCom { public static void main(String args[]) { TickTock tt = new TickTock; MyThread mtl = new MyThread("Tick", tt); MyThread mt2 = new MyThread("Tock", tt); try { mtl.thrd.join; mt2.thrd.join; } catch(InterruptedException exc) { System.out.println("Main thread interrupted."); } } }
В
результате выполнения этой программы на экране появляются следующие сообщения: Tick Tock Tick Tock Tick Tock Tick Tock Tick Tock `Рассмотрим более подробно исходный код программы, имитирующей работу часов. В ее основу положен класс TickTock. В нем содержатся два метода tick и tock , которые взаимодействуют друг с другом. Это взаимодействие организовано таким образом, чтобы за словом "Tick” всегда следовало слово "Tock", затем слово "Tick" и т.д. Обратите внимание на переменную state. В процессе работы имитатора часов в данной переменной хранится строка "ticked" или "tocked", определяющая текущее состояГлава 1 1. Многопоточное программирование 41.1 ние часов после такта “тик” или/‘так” соответственно. В методе main создается объект tt типа TickTock, используемый для запуска двух потоков на исполнение.
Потоки строятся на основе объектов типа MyThread. Конструктору MyThread передаются два параметра. Первый из них задает имя потока (в данном случае — "Tick" или "Тоск"), а второй — ссылку на объект типа TickTock (в данном случае — объект tt). В методе run из класса MyThread вызывается метод tick , если поток называется "Tick", или же метод tock, если поток называется "Тоск". Каждый из этих методов вызывается пять раз с параметром, принимающим логическое значение true. Работа имитатора часов продолжается до тех пор, пока методу передается параметр с логическим значением true. Последний вызов каждого из методов с параметром, принимающим логическое значение false, останавливает имитатор работы часов.
Самая важная часть программы находится в теле методов tick и tock из класса TickTock. Начнем с метода tick . Для удобства анализа ниже представлен исходный код этого метода. synchronized void tick(boolean running) { if(!running) { // остановить часы state = "ticked"; notifyO; // уведомить ожидающие потоки return; } System.out.print("Tick "); state = "ticked"; // установить текущее состояние после такта "тик" notify; // уведомить метод tock о возможности продолжить выполнение try { while(!state.equals("tocked") ) wait; // ожидать завершения метода tock } catch(InterruptedException exc) { System.out.println("Thread interrupted."); } }
Прежде всего обратите внимание на то, что в объявлении метода tick присутствует ключевое слово synchronized, указываемое в качестве модификатора доступа. Как пояснялось ранее, действие методов wait и notify распространяется только на синхронизированные методы. В начале метода tick проверяется значение параметра running. Этот параметр служит для корректного завершения программы, имитирующей работу часов. Если он принимает логическое значение false, имитатор работы часов должен быть остановлен. Если же параметр running принимает логическое значение true, а переменная state — значение "ticked", вызывается метод notify , разрешающий ожидающему потоку возобновить свое исполнение. Мы еще вернемся к этому вопросу несколько ниже.
По ходу работы имитируемых часов в методе tick выводится слово "Tick", переменная state принимает значение "ticked", а затем вызывается метод notify . Вызов метода notify возобновляет исполнение ожидающего потока. Далее в цикле while вызывается метод wait . В итоге выполнение метода tick будет приостановлено до тех пор, пока другой поток не вызовет метод notify . Таким образом, очередной шаг цикла не будет выполнен до тех пор, пока другой поток не вызовет метод notify для того же самого объекта. Поэтому когда вызывается метод tick , на экран выводится слово "Tick" и другой поток получает возможность продолжить свое исполнение, а затем выполнение этого метода приостанавливается.
В том цикле while, в котором вызывается метод wait , проверяется значение переменной state. Значение "tocked", означающее завершение цикла, будет установлено только после выполнения метода tock . Этот цикл предотвращает продолжение исполнения потока в результате ложной активизации. Если по окончании ожидания в переменной state не будет присутствовать значение "tocked", значит, имела место ложная активизация, и метод wait будет вызван снова.
Метод tock является почти точной копией метода tick . Его отличие состоит лишь в том, что он выводит на экран слово "Tock" и присваивает переменной state значение "tocked". Следовательно, когда метод tock вызывается, он выводит на экран слово "Tock", вызывает метод notify , а затем переходит в состояние ожидания. Если проанализировать работу сразу двух потоков, то станет ясно, что за вызовом метода tick тотчас следует вызов метода tock , после чего снова вызывается метод tick , и т.д. В итоге оба метода синхронизируют друг друга.