Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

TarasovVLJavaAndEclipse_10_MultiThreads

.pdf
Скачиваний:
10
Добавлен:
08.04.2015
Размер:
836.77 Кб
Скачать

}

catch(InterruptedException e) { System.out.println ("Прерывание") ;

}

}

}

Вывод этой программы:

[Привет[Синхронизированный[Мир]

]

]

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

Чтобы стабилизировать предшествующую программу, нужно сериализовать (преобразовать в последовательную форму) доступ к методу call(). То есть требуется организовать доступ к нему только одного потока одновременно. Для этого нужно просто в начало определения метода call() вставить ключевое слово synchronized, например, как в следующем фрагменте:

class Callme {

synchronized void call(String msg) {

Это предохраняет потоки от ввода call(), пока его использует другой поток. После добавления synchronized вывод программы становится таким:

[Привет] [Синхронизированный] [Мир]

[Привет] [Мир]

[Синхронизированный]

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

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

Оператор synchronized

Хотя определения синхронизированных методов внутри классов — это простые и эффективные средства достижения синхронизации, они не будут работать во всех случаях. Чтобы понять, почему так происходит, рассмотрим следующую ситуацию. Пусть нужно синхронизировать доступ к объектам класса, который не был разработан для многопоточного доступа. То есть класс не использует синхронизированные методы. Кроме того, этот класс был создан третьим лицом, и нет доступа к исходному коду. Таким образом, нет возможности добавлять спецификатор synchronized к соответствующим методам в классе. Для синхронизации доступа к объекту этого класса нужно поместить вызовы методов, определенных этим классом внутрь синхронизированного блока. Вот общая форма оператора synchronized:

synchronized(object) {

// операторы для синхронизации

}

где object — ссылка на объект, который нужно синхронизировать. Если нужно синхронизировать одиночный оператор, то фигурные скобки можно опустить. Блок гарантирует, что вызов метода, который является членом объекта object, происходит только после того, как текущий поток успешно ввел монитор объекта.

Вот альтернативная версия предыдущего примера, использующая синхронизированный блок в методе run():

Программа 65. Синхронизированный блок

//Файл Synch1.java

//Эта программа использует синхронизированный блок,

class Callme {

void call(String msg) { System.out.print("[" + msg); try {

Thread.sleep(1000);

}

catch (InterruptedException e) { System.out.println("Прерывание") ;

}

System.out.println("]");

}

}

class Caller implements Runnable {

String msg; Callme target; Thread t;

public Caller(Callme targ, String s) { target = targ;

msg = s;

t = new Thread(this); t.start ();

}

// синхронизировать обращения к call() public void run() {

synchronized(target) { // синхронизированный блок target.call (msg);

}

}

}

class Synch1 {

public static void main(String args[]) { Callme target = new Callme ();

Caller ob1 = new Caller(target, "Привет");

Caller ob2 = new Caller(target, "Синхронизированный"); Caller ob3 = new Caller(target, "Мир");

// ждать завершения потоков try {

ob1.t.join(); ob2.t.join (); ob3.t.join();

}

catch(InterruptedException e) { System.out.println ("Прерывание") ;

}

}

}

Здесь метод call() не модифицируется ключевым словом synchronized. Вместо этого внутри класса caller метода run() используется оператор synchronized. Он выполняет такой же правильный вывод, как в предыдущем примере, потому что каждый поток ожидает завершения предшествующего потока перед своим продолжением.

1.11. Межпоточные связи

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

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

циклом, который используется для повторяющейся проверки некоторого условия. Как только условие становится истинным, предпринимается соответствующее действие. На этом теряется время CPU. Например, рассмотрим классическую проблему организации очереди, где один поток производит некоторые данные, а другой — их потребляет. Чтобы сделать задачу более интересной, предположим, что, прежде чем генерировать большее количество данных, производитель должен ждать, пока потребитель не закончит свою работу. В системе же опроса, потребитель тратил бы впустую много циклов CPU на ожидание конца работы производителя. Как только производитель закончил свою работу, он вынужден начать опрос, затрачивая много циклов CPU на ожидание конца работы потребителя. Ясно, что такая ситуация нежелательна.

Чтобы устранить опросы, Java содержит изящный механизм межпроцессовой связи через методы wait(), notify() и notifyAll(). Они реализованы как final-методы в классе object, поэтому доступны всем классам. Все три метода можно вызывать только внутри синхронизированного метода. Правила для использования этих методов:

wait() сообщает вызывающему потоку, что нужно уступить монитор и переходить в режим ожидания ("спячки"), пока некоторый другой поток не введет тот же монитор и не вызовет notify();

notify() "пробуждает" первый поток (который вызвал wait()) на том же самом объекте;

notifyAll() пробуждает все потоки, которые вызывали wait(), на том же самом объекте. Первым будет выполняться самый высокоприоритетный поток.

Эти методы объявляются в классе object в следующей форме:

final void wait() throws InterruptedException; final void notify();

final void notifyAll()

Существуют дополнительные формы wait(), которые позволяют указать период времени ожидания.

Следующий пример программы неправильно реализует простую форму задачи производитель/потребитель. Программа состоит из четырех классов: Q — очередь, которую вы пробуете синхронизировать; Producer — поточный объект, который производит элементы ввода в

очередь; consumer — поточный объект, который потребляет элементы ввода очереди; и PC — крошечный класс, который создает одиночные классы Q, Producer и Consumer.

Программа 66. Некорректная связь производителя и потребителя

//Файл PC.java

//Некорректная реализация производителя и потребителя.

class Q { int n;

synchronized int get() { System.out.println("Получено: " + n); return n;

}

synchronized void put(int n) { this.n = n;

System.out.println("Отдано: " + n);

}

}

class Producer implements Runnable { Q q;

Producer(Q q) { this.q = q;

new Thread(this, "Producer").start();

}

public void run() { int i = 0; while(true) {

q.put(i++);

}

}

}

class Consumer implements Runnable { Q q;

Consumer (Q q) { this.q = q;

new Thread(this, "Consumer").start();

}

public void run() { while(true) { q.get () ;

}

}

}

class PC {

public static void main(String args[]) { Q q = new Q();

new Producer(q); new Consumer(q);

System.out.println("Для прерывания нажмите Control-C.");

}

}

Хотя методы put() и get() класса Q синхронизированы, ничто не останавливает производителя от переполнения потребителя, так же, как ничто не будет останавливать потребителя от потребления одного и того же значения очереди дважды. Таким образом, получаеся

следующий ошибочный вывод (точный вывод будет меняться в

зависимости от быстродействия процессора и загруженности задачами):

Отдано: 1 Получено: 1 Получено: 1 Получено: 1 Получено: 1 Получено: 1 Отдано: 2 Отдано: 3 Отдано: 4 Отдано: 5 Отдано: б Отдано: 7 Получено: 7

Заметьте, что после выдачи производителем первого элемента (1) потребитель запускается и получает ту же 1 пять раз (это видно в последовательных строчках вывода). Затем производитель возобновляет работу и производит элементы от 2 до 7, не оставляя потребителю шансов для их потребления.

Для получения корректной версии этой программы необходимо использовать методы wait() и notify(), чтобы сигнализировать в обоих направлениях, как показано ниже:

Программа 67. Корректная связь производителя и потребителя

//ФАйл PCFixed.java

//Корректная реализация поставщика и потребителя.

class Q { int n;

boolean valueSet = false; synchronized int get() {

if(!valueSet) try {

wait();

}

catch(InterruptedException e) { System.out.println("InterruptedException выброшено");

}

System.out.println("Получено: " + n); valueSet = false;

notify(); return n;

}

synchronized void put(int n) { if(valueSet)

try {

wait () ;

}

catch(InterruptedException e) {

System.out.println("InterruptedException выброшено");

}

this.n = n; valueSet = true;

System.out.println("Отдано: " + n); notify ();

}

}

class Producer implements Runnable { Q q;

Producer(Q q) { // Производитель связывается с очередью this.q = q;

new Thread(this, "Producer").start();

}

public void run() { int i = 0; while(true) {

q.put(i++);

}

}

}

class Consumer implements Runnable { Q q;

Consumer(Q q) { this.q = q;

new Thread(this, "Consumer").start();

}

public void run() { while(true) { q.get ();

}

}

}

class PCFixed {

public static void main(String args[]) { Q q = new Q();

new Producer(q); new Consumer(q);

System.out.println("Для прерывания нажмите Control-С.");

}

}

Внутри get() вызывается wait(). Это приостанавливает выполнение get(), пока Producer не уведомит его, что некоторые данные готовы. Когда это случается, выполнение внутри get() возобновляется. После того как данные были получены, get() вызывает notify(). Тот сообщает объекту Producer, что можно дальше размещать данные в очереди. Внутри put() метод wait() приостанавливает выполнение, пока Consumer не удалил элемент из очереди. Когда выполнение возобновляется, в очередь помещается следующий элемент данных и вызывается notify(). Это извещает Consumer, что тот должен теперь удалить элемент.

Вывод этой программы показывает ее чисто синхронное поведение:

Отдано 1 Получено 1 Отдано 2 Получено 2 Отдано 3 Получено 3 Отдано 4 Получено 4 Отдано 5 Получено 5

1.12. Блокировка

Специальный тип ошибки, которую нужно избегать и которая специально относится к многозадачности, это — (взаимная) блокировка. Она происходит, когда два потока имеют циклическую зависимость от пары синхронизированных объектов. Например, предположим, что один поток вводит монитор в объект х, а другой поток вводит монитор в объект у. Если поток в х пробует вызвать любой синхронизированный метод объекта у, это приведет к блокировке, как и ожидается. Однако если поток в у, в свою очередь, пробует вызвать любой синхронизированный метод объекта х, то он будет всегда ждать, т. к. для получения доступа к х, он был бы должен снять свою собственную блокировку с у, чтобы первый поток мог завершиться. Взаимоблокировка — трудная ошибка для отладки по двум причинам:

Вообще говоря, она происходит очень редко, когда интервалы временного квантования двух потоков находятся в определенном соотношении.

Она может включать больше двух потоков и синхронизированных o6ъектов. (То есть блокировка может происходить через более замысловатые последовательность событий, чем только что описано.)

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

Сдующий пример создает два класса (A и B) с методами foo() и bar(), соответственно, которые делают краткую паузу перед попыткой вызвать методы другого класса. Главный класс (с именем Deadlock) создает экземпляры типа A и B, и затем стартует второй поток для установки состояния блокировки. Оба метода используют sleep() для вызова состояния блокировки.

Программа 68. Блокировка

//Файл Deadlock.java

//Пример блокировки.

class A {

synchronized void foo(B b) {

String name = Thread.currentThread().getName(); System.out.println(name + " вошел в A.foo");

try { Thread.sleep(1000);

}

catch(Exception e) { System.out.println("A прерван");

}

System.out.println(name + " пытается вызвать В.last()"); b.last();

}

synchronized void last() { System.out.println("Внутри A.last") ;

}

}

class B {

synchronized void bar(A a) {

String name = Thread.currentThread().getName(); System.out.println(name + " вошел в В.bar"); try {

Thread.sleep(1000);

}

catch(Exception e) { System.out.println("В прерван");

}

System.out.println(name + " пытается вызвать A.last()"); a.last();

}

synchronized void last() { System.out.println("Внутри A.last");

}

}

class Deadlock implements Runnable { A a = new A();

В b = new B(); Deadlock() {

Thread.currentThread().setName("MainThread"); Thread t = new Thread (this, "RacingThread"); t.start();

a.foo(b); // Получить блокировку на а в этом потоке

System.out.println("Возврат в главный поток");

}

 

public void run() {

 

b.bar(a);

// Получить блокировку на b в другом потоке

System.out.println("Возврат в другой поток");

}

public static void main(String args[]) { new Deadlock();

}

}

Когда вы выполните эту программу, то увидите следующий вывод:

MainThread вошел в A.foo

RacingThread вошел в В.bar

MainThread пытается вызвать В.last()

RacingThread пытается вызвать A.last()

Поскольку программа была блокирована, вы должны нажать клавиши <Ctrl>+<C>, чтобы закончить программу. Вы можете увидеть полный кэш-дамп потока и монитора, если нажмете клавиши

<Ctrl>+<Break> на PC (или <Ctrl>+<\> на Solaris). Вы увидите, что RacingThread имеет монитор на b, в то время как он ожидает монитор на а. В то же самое время, MainThread имеет а и ожидает получения b. Эта программа никогда не будет завершена. Как показывает данный пример, если ваша многопоточная программа иногда блокируется, то блокировка — это одно из первых состояний, которое вы должны проверить.

1.13. Приостановка, возобновление и остановка потоков

Приостановка выполнения потока иногда полезна. Например, отдельные потоки могут использоваться, чтобы отображать время дня. Если пользователь не хочет видеть отображения часов, то их поток может быть приостановлен. В любом случае приостановка потока — простое дело. После приостановки перезапуск потока также не сложен.

Механизмы приостановки, остановки и возобновления потоков различны для Java 2 и более ранних версий Java. Хотя для всего нового кода следует использовать подход Java 2, необходимо все-таки понять, как подобные операции выполнялись в более ранних средах Java. Например, вам требуется модифицировать или поддерживать программы старых версий. Вы также должны понимать, почему были сделаны изменения для Java 2. По этим причинам, следующий раздел описывает первоначальный способ управления выполнением потока, а следующий — новый подход, используемый в Java 2.

Приостановка, возобновление и остановка потоков в Java 1.1 и более ранних версиях

До Java 2 для приостановки и перезапуска выполнения потока программа использовала методы suspend() и resume(), которые определены в классе Thread. Они имеют такую форму:

final void suspend () final void resume ()

Представленная далее программа демонстрирует эти методы:

Программа 69. Приостановка и возобновление потоков с использование suspend() и resume()

//Файл SuspendResume.java

//Использование suspend() и resume().