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

Джош Блох

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

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

private FieldType field; synchronized FieldType getField() {

if (field == null)

field - computeFieldValue(); return field;

}

Обе эти идиомы (нормальная инициализация и отложенная ини­ циализация с синхронизированным методом доступа) являются не­ изменными, когда используются на статическом поле, кроме того, что вы добавляете модификатор static к полю и декларацию метода доступа.

Если вам требуется отложенная инициализация статического поля для производительности, используйте идиому, содержащую отложенную инициализацию класса. Эта идиома (также известная как идиома, содержащая инициализацию класса по запросу) исполь­ зует гарантию того , что класс не будет инициализирован до тех пор, ока не будет использован [JLS 12.4.1]. Вот как она выглядит.

// Идиома отложенной инициализации класса для статического поля private static class FieldHolder {

static final FieldType field = computeFieldValue();

}

static FieldType getFieldO { return FieldHolder. field; }

Когда впервые запускается метод get Field, он впервые читает поле FieldHolder. field, запуская инициализацию класса FieldHolder. Красота данной идиомы заключается в том, что метод getFieldHe син­ хронизирован и выполняет только доступ к полю, так что отложенная инициализация практически ничего не добавляет к затратам на доступ. Современная виртуальная машина синхронизирует доступ к полям только при инициализации класса. Как только класс инициализирован, виртуальная машина изменяет код, чтобы последующий доступ к полю не содержал в себе никакой проверки или синхронизации.

Если вам требуется отложенная инициализация для повы­ шения производительности поля экземпляра, используйте иди­

390

С тать я 71

ому двойной проверки. Эта идиома избавляет от затрат на блоки­ ровку при доступе к полю после того, как оно инициализировано (статья 67). Идея, заложенная в данной идиоме, заключается в том, чтобы дважды проверить значение поля (отсюда и название): один раз без блокировки, в случае если поле окажется неинициализиро­ ванным, и второй раз с блокировкой. Только если вторая проверка покажет, что поле не инициализировано, выполняется его инициали­ зация. Поскольку нет блокировки, если поле уже инициализировано, то важно, чтобы поле было объявлено volatile (статья 66). Вот как выглядит идиома:

//Идиома двойной проверки для отложенной инициализации поля

//экземпляра

private volatile FieldType field; FieldType getFieldO {

FieldType result = field;

if (result == null) { // First check (no locking) synchronized(this) {

result = field;

if (result == null) // Second check (with locking) field = result = computeFieldValue();

}

}

return result;

}

Этот код может показаться довольно запутанным. В частности, может быть непонятной необходимость локальной переменной re­ sult. Эта переменная убеждается в том, что field читается только один раз в общем случае, где она уже инициализирована. Хотя в ней и нет острой необходимости, это может улучшить производитель­ ность и более элегантно с точки зрения стандартов низкоуровнево­ го потокового программирования. На моей машине метод работал на 25% быстрее, чем обычная версия без локальной переменной.

До релиза 1.5 идиома двойной проверки не работала надежно, потому что семантика модификатора volatile не была достаточно

391

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

сильно определена для обеспечения надежности [PughOl]. Модель памяти, представленная в версии 1.5, решила проблему [JLS, 17; Goetz06, 16]. Сегодня идиома двойной проверки является хорошим приемом для отложенной инициализации поля. Хотя можно выпол­ нять двойную проверку на статическом поле, нет причин, чтобы так делать: идиома отложенной инициализации класса больше подходит для этого.

Два варианта идиомы двойной проверки заслуживают внимания. В некоторых случаях может понадобиться отложенная инициализа­ ция поля экземпляра, которая допускает повторную инициализацию. Если вы окажетесь в такой ситуации, то можно использовать вари­ ант, где применяется идиома двойной проверки, которая обходится без второй проверки. Она известна под названием идиома одно­ кратной проверки. Вот как она выглядит. Обратите внимание, что поле field снова объявляется как volatile:

//Идиома однократной проверки - может вызвать повторную

//инициализацию!

private volatile FieldType field; private FieldType getFieldO {

FieldType result = field; if (result == null)

field = result = computeFieldValue();

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

Если для вас не важно, чтобы каждый поток пересчитывал зна­ чение поля, и если тип поля является примитивным, но не long или double, то можно убрать модификатор volatile из декларации поля

видиоме однократной проверки. Этот вариант известен как специ­ фичная идиома однократной проверки. Она ускоряет доступ к полю

внекоторых архитектурах за счет дополнительных инициализаций

392

С тать я 72

(до одной на поток, получающий доступ к полю). Это определен­ но экзотический прием, не для каждодневного использования. Но, тем не менее, он используется экземпляром String для кэширования хэш-кодов.

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

Не попадайте в зависимость от планировщика потоков

Когда в системе выполняется несколько потоков, соответству­ ющий планировщик определяет, какие из них будут выполняться и в течение какого времени. Каждая правильная реализация JV M при этом будет пытаться добиться какой-то справедливости, одна­ ко конкретная стратегия диспетчеризации в различных реализациях отличается очень сильно. Соответственно, хорошо написанные мно­ гопоточные приложения не должны зависеть от особенностей этой стратегии. Любая программа, чья корректность или производи­ тельность зависит от планировщика потоков, скорее всего, пере­ носимой не будет.

Лучший способ написать устойчивую, гибкую и переносимую многопоточную программу — обеспечить условия, когда в любой мо­ мент времени может выполняться несколько потоков. В этом случае планировщику потоков остается совсем небольшой выбор: он про­ сто передает управление выполняемым потокам, пока те еще могут

393

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

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

Основной прием, позволяющий сократить количество запущен­ ных потоков, заключается в том, чтобы каждый поток выполнял не­ большую порцию работы, а затем ждал следующей. С точки зрения Executor Framework (статья 68) это означает правильное определе­ ние размера пула потоков [Goetz06. 8.2], и надо делать так, что­ бы задачи были небольшими и не зависели друг от друга. Задачи не должны быть слишком маленькими, иначе затраты на диспетче­ ризацию негативно отразятся на производительности.

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

ную реализацию CountDownLatch:

//Ужасная реализация CountDownLatch - состояние активного

//ожидания не прерывается!

public class SlowCountDownLatch { private int count;

public SlowCountDownLatch(int count) { if (count < 0)

throw new IllegalArgumentException(count + “ < 0”); this.count = count;

}

public void await() { while (true) {

synchronized(this) {

if (count == 0) return;

}

394

С татья 72

}

}

public synchronized void countDown() { if (count != 0)

count--;

}

}

Ha моей машине SlowCountDownLatch выполняется в 2000 раз медленнее, чем CountDownLatch, когда в ожидании находятся 1000 по­ токов. Хотя данный пример может казаться и нереалистичным, не так редко можно встретить системы, в которых без надобности запускаются один или более потоков. Результат может быть не на­ столько критичным, как при SlowCountDownLatch, но производитель­ ность и переносимость, скорее всего, пострадают.

Столкнувшись с программой, которая едва работает из-за того, что некоторым потокам недостаточно времени Ц П по сравнению с другими потоками, не поддавайтесь искушению починить про­ грамму добавлением вызова Thread.yield. Вам, может, удастся заставить работать программу, но она не будет переносимой. Один и тот же запуск yield может увеличить производительность на од­ ной реализации виртуальной машины, но на второй увеличение будет хуже, а на третьей вообще не будет иметь никакого эффекта. Пр о- верить семантику Thread.yield невозможно. Лучшим подходом будет изменить структуру приложения для снижения количества па­ раллельно выполняемых потоков.

Аналогичный прием, к которому также относится похожий недо­ статок, — это настройка приоритетности потоков. Приоритетность потоков —одна из наименее переносимых особенностей на плат­ форме Java. Разумным будет настроить отклик приложения, приме­ нив несколько свойств приоритетности потоков, но это редко бывает необходимо и делает приложения непереносимыми. Совсем неразум­ но для решения серьезных проблем с живучестью использовать при­ оритетность потоков. Проблема, скорее всего, будет актуальна до тех пор, пока вы не найдете и не устраните основное препятствие.

395

Th read.sleep(O),

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

В первой редакции этой книги говорилось, что единственное применение Thread.yield, используемое большинством програм­ мистов, — это искусственное увеличение параллельности для те­ стирования. Идея была в том, чтобы убрать ошибки путем иссле­ дования большой части пространства состояния программы. Этот прием когда-то был довольно эффективен, но никогда не была га­ рантирована его работа. В спецификации к Thread, yield говори­ лось, что он вообще ничего не делает, просто возвращает контроль над ним вызывающему. Некоторые виртуальные машины действи­ тельно так и делают. Следовательно, вам необходимо использовать Th read.sleep(1) вместо Th read.yield для проверки параллельности. Не используйте который может немедленно воз­ вратиться.

Подведем итоги. Правильность вашего приложения не должна зависеть от планировщика потоков. Иначе полученное приложение не будет устойчивым и переносимым. Как следствие, не надо свя­ зываться с методом Thread.yield и приоритетами. Эти функции предназначены только для планировщика. Их можно дозированно использовать для улучшения качества сервиса в уже работающей ре­ ализации, но ими никогда нельзя пользоваться для «исправления» программы, которая едва работает.

Избегайте группировки потоков

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

396

С татья 73

пасности для платформы Java 2 [G ong03], они даже не упоми­ наются.

Но если группировка потоков не несет никакой функциональ­ ной нагрузки в системе безопасности, то какие же функции она вы­ полняет?

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

По иронии судьбы, API ThreadGroup слаб с точки зрения под­ держки многопоточности. Чтобы для некоей группы получить пере­ чень активных потоков, вы должны вызвать метод enumerate. В ка­ честве параметра ему передается массив, достаточно большой, чтобы в него можно было записать все активные потоки. Метод activeCount возвращает количество активных потоков в группе, однако нет ни­ какой гарантии, что это количество не изменится за то время, пока вы создаете массив и передаете его методу enumerate. Если указан­ ный массив окажется слишком мал, метод enumerate без каких-либо предупреждений игнорирует потоки, не поместившиеся в массив.

Точно так же API некорректен, когда ему передается список подгрупп, входящих в группу потоков. И хотя указанные проблемы можно было решить, добавив в класс ThreadGroup новые методы, это­ го не было сделано из-за отсутствия реальной потребности. Группи­ ровка потоков сильно устарела.

До появления версии 1.5 существовал небольшой функционал, который был доступен только вместе с A PI ThreadGroup: метод

ThreadGroup. uncaughtException был единственным способом по­

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

дом setllncaughtExceptionHandler, относящимся к Thread.

Подведем итоги. Группировка потоков практически не имеет сколь-нибудь полезной функциональности, и большинство предо-

397

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

ставляемых ею возможностей имеет дефекты. Группировку потоков следует рассматривать как неудачный эксперимент, а существование групп можно игнорировать. Если вы проектируете класс, который работает с логическими группами потоков, вам нужно, вероятнее все­ го, использовать экзекуторы пула потоков (статья 68).

398

Гл а в а

^« э д ш ш ш ш ш ш ш ю ш и ш ш т и ш ш т м ^

Сериализация

В этой главе описывается API сериализации объекта (object serialization), который формирует среду для представления объек­ та в виде потока байтов и, наоборот, для восстановления объекта из соответствующего потока байтов. Процедура представления объекта в виде потока байтов называется сериализацией объекта (serializing),

обратный процесс называется его десериализацией (deserializing).

Как только объект был сериализован, его представление можно пере­ давать с одной работающей виртуальной машины Java на другую или сохранять на диске для последующей десериализации. Сериализация обеспечивает стандартное представление объектов на базовом уровне, которое используется для взаимодействия с удаленными машинами, а также как стандартный формат для сохранения данных при работе с компонентами JavaBeans. Замечательной особенностью данной главы является шаблон serialization proxy (статья 78), которая поможет вам избежать многие ловушки, связанные с сериализацией объекта.

Соблюдайте осторожность

при реализации интерфейса Serializable

Чтобы сделать экземпляры класса сериализуемыми, достаточно добавить в его декларацию слова «implements Serializable». По-

399

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