![](/user_photo/2706_HbeT2.jpg)
Джош Блох
.pdfГлава 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
Глава 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