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

Джош Блох

.pdf
Скачиваний:
57
Добавлен:
08.03.2016
Размер:
27.13 Mб
Скачать

Глава 10 Потоки

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

Рассмотрим задачу остановки одного потока из другого. У би­ блиотек есть метод Thread.stop, но он давно устарел и является не­ безопасным: работа с ним может привести к разрушению данных. Не используйте Thread.stop. Для остановки одного потока из дру­ гого рекомендуется использовать прием, который заключается в том, чтобы в одном потоке создать некое опрашиваемое поле, значение которого по умолчанию false, но может быть установлено true вто­ рым потоком, для указания, что первый поток должен остановить сам себя. Обычно такое поле имеет тип boolean. Поскольку чтение и за ­ пись такого поля атомарны, у некоторых программистов появляется соблазн предоставить ему доступ без синхронизации:

// Ошибка! - Как вы думаете, как долго будет выполняться эта программа? public class StopThread {

private static boolean stopRequested; public static void main(String[] args) throws InterruptedException {

Thread backgroundThread = new Thread(new RunnableO { public void run() {

int i = 0;

while (!stopRequested)

});

backgroundThread.start (); TimeUnit.SECONDS.sleep( 1); stopRequested = true;

}

Вы, возможно, думаете, что такая программа выполнится за се­ кунду, после чего поток поменяет значение stopRequested на true, что приведет к тому, что цикл фонового потока завершится. Тем не менее

360

С тать я 66

на моей машине программа никогда не завершается: цикл фонового потока выполняется вечно!

Проблема представленного кода заключается в том, что в отсут­ ствие синхронизации нет гарантии (если ее вообще можно дать), что поток, подлежащий остановке, «увидит», что основной поток поме­ нял значение stopRequested. В результате метод requestStop может оказаться абсолютно неэффективным. При отсутствии синхрониза­ ции для виртуальной машины приемлемо преобразовать данный код:

while (!stopRequested) i++;

в такой:

if (!stopRequested) while (true)

i++;

Эта оптимизация называется поднятием, и это именно то, что делает сервер виртуальной машины HotSpot. Результат — падение живучести: программе не удается успешно завершиться. Один из способов разрешить эту проблему — непосредственно синхрони­ зировать доступ к полю stopRequested:

// Правильно синхронизированное совместное завершение потока public class StopThread {

private static boolean stopRequested;

private static synchronized void requestStopO { stopRequested = true;

}

private static synchronized boolean stopRequestedO { return stopRequested;

}

public static void main(String[] args) throws InterruptedException {

Thread backgroundThread = new Thread(new RunnableO { public void run() {

int i = 0;

361

Глава 10 Потоки

while (!stopRequestedO) i++;

}

});

backgroundThread.start();

TimeUnit.SECONDS.sleep(1);

requestStopO;

Заметим, что и метод записи (requestStop), и метод чтения (stopRequested) синхронизированы. Синхронизировать только метод за ­ писи недостаточно. На самом деле от синхронизации нет пользы, если и операции чтения и записи не синхронизированы.

Действия синхронизированных методов StopTh read были бы ато­ марными даже без синхронизации. Другими словами, синхронизация этих методов используется исключительно для коммуникации, а не для взаимного исключения. Хотя затраты на синхронизацию каждой итерации цикла невелики, есть корректная альтернатива, которая не будет настолько нагружена текстом и производительность кото­ рой будет лучше. Блокировку во второй версии StopTh read можно, если stopRequested будет объявлен непостоянным (volatile). Хотя модификатор volatile не выполняет взаимного исключения, он гаран­ тирует, что любой поток, который прочитает поле, увидит недавно записанные значения:

// Совместное завершение потоков с помощью поля volatile public class StopTh read {

private static volatile boolean stopRequested; public static void main(String[] args)

throws InterruptedException {

Thread backgroundThread = new Thread(new RunnableO { public void run() {

int i = 0;

while (!stopRequested)

362

363
synchro-

С татья 66

background’llread.start(); TimeUnit.SECONDS.sleep(1); stopRequested = true;

}

}

Использовать поле volatile нужно с осторожностью. Рассмотрим следующий метод, который должен генерировать серийный номер:

// Ошибка - требуется синхронизация!

private static volatile int nextSerialNumber = 0; public static int generateSerialNumber() {

return nextSerialNumber++;

}

Этот метод должен гарантировать, что при каждом вызове бу­ дет возвращаться другой серийный номер до тех пор, пока не будет возвращено иное значение (пока не будет произведено 232 вызова). Состояние генератора включает лишь одно атомарно записываемое поле (nextSerialNumber), для которого допустимы любые возможные значения. Для защиты инвариантов данного генератора серийных но­ меров синхронизация не нужна. Тем не менее без синхронизации этот метод не работает.

Проблема в том, что оператор приращения ( + + ) атомарным не является. Он выполняет две операции в поле nextSerialNumber: сна­ чала он читает его значение, потом записывает новое значение, равное старому плюс один. Если второй поток читает значение между перио­ дами чтения первым потоком старых и записи новых значений, второй поток будет видеть те же значения, что и первый, и возвращать тот же серийный номер. Чтение и запись — это независимые операции, которые выполняются последовательно. Соответственно, несколько параллельных потоков могут наблюдать в поле nextSerialNumber одно и то же значение и возвращать один и тот же серийный номер. Это ошибка безопасности: программа считает неверные результаты.

Одним из способов исправления метода generateSerialNumber сводится к простому добавлению в его декларацию слова

Глава 10 • Потоки

nized. Тем самым гарантируется, что различные вызовы не будет смешиваться и каждый новый вызов будет видеть результат обра­ ботки всех предыдущих обращений. После того как вы сделаете это, вам необходимо удалить модификатор volatile из nextSerialNum- ber. Чтобы сделать этот метод «железобетонным», возможно, имеет смысл заменить int на long или инициировать какое-либо исключение если nextSerialNumber будет близко к переполнению.

Все же лучше последовать совету из статьи 47 и использо­

вать класс AtomicLong, являющийся частью java.concurrent.atomic.

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

generalSerialNumbeг:

private static final AtomicLong nextSerialNum = new AtomicLong(); public static long generateSerialNumber() {

return nextSerialNum.getAndlncrement();

}

Лучший способ избежать проблем, описанных в этой статье, — не делать общими изменяемые данные. Либо делать общими неизме­ няемые данные (статья 15), либо вообще общими ничего не делать. Другими словами, изменяемые данные держите в одном потоке. Если вы примените данную политику, необходимо это документи­ ровать, чтобы при развитии программы это можно было сохранить. Также необходимо глубокое понимание структур и библиотек, кото­ рыми вы пользуетесь, так как они могут запускать потоки, о которых вы можете не знать.

Вполне допустимо, если один поток изменит объект данных на некоторое время, а затем сделает его общим для других потоков, синхронизируя только действие обобщения ссылки на объект. Другие потоки тогда смогут читать объект без дальнейшей синхронизации до тех пор, пока он снова не изменится. Такие объекты называют­ ся эффективно неизменяемыми [Goetz06, 3.5.411]. Преобразова­ ние такой ссылки на объект от одного потока на другой называется безопасной публикацией [Goez06, 3.5.3]. Существует много путей

364

С тать я 67

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

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

Без синхронизации невозможно дать какую-либо гарантию, что из­ менения в объекте, сделанные одним потоком, были увидены другим. Несинхронизированный доступ к данным может привести к отказам, затрагивающим живучесть и безопасность системы. Воспроизвести такие отказы бывает крайне сложно. Они могут зависеть от време­ ни и быть чрезвычайно чувствительны к деталям реализации JV M и особенностям компьютера. Если вам требуется только связь между потоками, модификатор volatile является подходящей формой син­ хронизации, но может быть довольно запутанным, если использовать его некорректно.

Избегайте избыточной синхронизации

Статья 66 предупреждает об опасностях недостаточной син­ хронизации. Данная статья посвящена обратной проблеме. В за ­ висимости от ситуации избыточная синхронизация может при­ водить к снижению производительности приложения, взаимной блокировке потоков или даже к непредсказуемому поведению программы.

Для исключения возможности взаимной блокировки ( deadlock) никогда не передавайте управление клиенту, если на­ ходитесь в синхронизированном методе или блоке. Иными сло­ вами, из области синхронизации не следует вызывать открытые или

365

Глава 10 Потоки

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

Для пояснения рассмотрим класс, который реализует упаков­ щик набора Observable. Он позволяет клиентам подписываться на уведомления каждый раз, когда к набору добавляется элемент. Это шаблон Observable [Gamma95, стр. 293]. Для краткости, класс не предоставляет уведомлений, когда элементы удаляются из набора, но настроить такие уведомления не составит труда. Этот класс реа­ лизуется поверх ForwardingSet из статьи 16, который можно реали­ зовывать повторно:

// Ошибка - запускает чужой метод из синхронизированного блока! public class ObservableSet<E> extends ForwardingSet<E> {

public ObservableSet(Set<E> set) { super(set); } private final List<SetObserver<E>> observers = new ArrayList<SetObserver<E>>();

public void addObserver(SetObserver<E> observer) { synchronized(observers) {

observers.add(observer);

}

}

public boolean removeObserver(SetObserver<E> observer) { synchronized(observers) {

return observers.remove(observer);

}

}

private void notifyElementAdded(E element) { synchronized(observers) {

for (SetObserver<E> observer : observers) observer.added(this, element);

366

С татья 67

(©Override public boolean add(E element) { boolean added = super.add(element); if (added)

notifyElementAdded(element); return added;

}

(©Override public boolean addAll(Collection<? extends E> c) { boolean result = false;

for (E element : c)

result |= add(element); // calls notifyElementAdded return result;

}

}

Наблюдатели подписываются на уведомления, запуская ме­ тод addObserver, и отписываются запуском метода removeObserver. В обоих случаях экземпляр интерфейса обратного вызова (callback) передается методу:

public interface SetObserver<E> {

// Invoked when an element is added to the observable set void added(ObservableSet<E> set, E element);

}

При кратком взгляде ObserverSet работает. Например, следую­ щая программа печатает числа от 0 до 99:

public static void main(St ring[] args) { ObservableSet<Integer> set =

new ObservableSet<Integer>(new HashSet<Integer>()); set.addObserver(new SetObserver<Integer>() {

public void added(ObservableSet<Integer> s, Integer e) { System.out.println(e);

}

});

for (int i = 0; i < 100; i++) set.add(i);

}

367

Глава 10 Потоки

Теперь попробуем нечто более необычное. Предположим, мы за­ меним вызов addObserver другим методом, который передает наблю­ дателю, который печатает значение Integer, которое было добавлено к набору и удаляет его, если значение равно 23:

set.addObserver(new SetObserver<Integer>() {

public void added(ObservableSet<Integer> s, Integer e) { System.out.println(e);

if (e == 23) s.removeObserver(this);

}

});

Вы можете подумать, что программа напечатает числа от О до 23, после чего наблюдатель отписывается и программа тихо за­ вершает работу. Но на самом деле она выводит значения от 0 до 23, после чего выводит ошибку currentModificationException. Проблема в том, что notifyElementAdded находится в процессе итерации поверх списка observers, когда запускается метод для наблюдателя added.

Метод added вызывает метод removeObserver набора Observable, ко­

торый в свою очередь вызывает observer, remove. Теперь у нас про­ блема. Мы пытаемся удалить элемент из списка во время итерации над ним, что недопустимо. Итерация метода notifyElementAdded на­ ходится в синхронизированном блоке, чтобы воспрепятствовать па­ раллельному изменению, но он не препятствует потоку, в котором происходит итерация, делать обратный вызов к набору observable и изменению его списка observers.

Попробуем теперь нечто странное: напишем попытку наблюда­ теля отписаться, но вместо непосредственного вызова removeObserver мы для этого воспользуемся услугами другого потока. Наблюдатель использует службу executor (статья 68).

// Наблюдатель, использующий без надобности фоновый поток set.addObserver(new SetObserver<Integer>() {

public void added(final ObservableSet<Integer> s, Integer e) { System.out.println(e);

if (e == 23) {

368

С тать я 67

ExecutorService executor = Executors.newSingleThreadExecutor();

final SetObserver<Integer> observer = this; try {

executor.submit(new Runnable() { public void run() {

s.removeObserver(observer);

}

}) •get();

} catch (ExecutionException ex) {

throw new AssertionError(ex.getCause());

}catch (InterruptedException ex) { throw new AssertionError(ex);

}finally {

executor.shutdown();

Ha этот раз ошибка исключения не выводится — мы получаем блокировку. Фоновый поток вызывает s. removeObse rve г, который пытается заблокировать observers, но он не может применить бло­ кировку, потому что основной поток ее уже применил. В любом слу­ чае основной поток ждет, когда фоновый поток завершит удаление наблюдателя, что объясняет блокировку.

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

вреальной системе, такой как набор GUI.

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

369

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]