- •Содержание
- •1. Введение
- •2. Создание и уничтожение объектов
- •3. Методы, общие для всех объектов
- •4. Классы и интерфейсы
- •5. Замена конструкций на языке c
- •6. Методы
- •7. Общие вопросы программирования
- •8. Исключения
- •9. Потоки
- •10. Сериализация
- •11. Литература
- •Глава 1
- •Глава 2 Создание и уничтожение объектов
- •Рассмотрите возможность замены конструкторов статическими методами генерации.
- •Свойство синглтона обеспечивайте закрытым конструктором
- •Отсутствие экземпляров обеспечивает закрытый конструктор
- •Не создавайте дублирующих объектов
- •Уничтожайте устаревшие ссыпки (на объекты)
- •Остерегайтесь методов flnalize
- •Глава 3 Методы, общие для всех объектов
- •Переопределяя метод equals, соблюдайте общие соглашения
- •Переопределяя метод equals, всегда переопределяйте hashCode
- •Всегда переопределяйте метод toStrlng
- •Подумайте над реализацией интерфейса ComparabIe
- •Глава 4 Классы и интерфейсы
- •Сводите к минимуму доступность классов и членов
- •Предпочитайте постоянство
- •Предпочитайте компоновку наследованию
- •Предпочитайте интерфейсы абстрактным классам.
- •Используйте интерфейсы только для определения типов
- •Предпочитайте статические классы-члены нестатическим
- •Глава 5
- •3Амена констрvкций на языке с
- •3Аменяйте структуру классом
- •3Амеияйте объедииеиие иерархией классов
- •/* Помещает данные в одно из полей объединения ... */
- •3Аменяйте конструкцию enum классом
- •Указатель на функцию заменяйте кпассом и интерфейсом
- •Глава 6 Методы
- •Проверяйте достоверность параметров
- •При необходимости создавайте резервные копии
- •Тщательно проектируйте сигнатуру метода
- •Перегружая методы, соблюдайте осторожность
- •Возвращайте массив нулевой длины, а не null
- •Для всех открытых элементов арi пишите dос - комментарии
- •Глава 7 Общие вопросы программирования
- •Сводите к минимуму область видимости локальных переменных
- •Изучите библиотеки и пользуйтесь ими
- •Не используйте строку там, где более уместен иной тип
- •При конкатенации строк опасайтесь потери производительности
- •Для ссыпки на объект используйте его интерфейс
- •Соблюдайте осторожность при использовании машинно-зависимых методов
- •Соблюдайте осторожность при оптимизации
- •Глава 8 Исключения
- •Используйте исключения лишь в исключительных ситуациях
- •Применяйте обрабатываемые исключения для восстановления, для программных ошибок используйте исключения времени выполнения
- •Избегайте ненужных обрабатываемых исключений
- •Предпочитайте стандартные исключения
- •Инициируйте исключения, соответствующие абстракции
- •Для каждого метода документируйте все инициируемые исключения
- •В описание исключения добавляйте информацию о сбое
- •Добивайтесь атомарности методов по отношению к сбоям
- •Не игнорируйте исключений
- •Глава 9 Потоки
- •Синхронизируйте доступ потоков к совместно используемым изменяемым данным
- •Избегайте избыточной синхронизации
- •Никогда не вызывайте метод wait вне цикла
- •Не попадайте в зависимость от планировщика потоков
- •При работе с потоками документируйте уровень безопасности
- •Избегайте группировки потоков
- •Глава 10 Сериализация
- •Соблюдайте осторожность при реализации интерфейса SerializabIe
- •Рассмотрите возможность использования специализированной сериализованной формы
- •Метод readObject должен создаваться с защитой
- •При необходимости создавайте метод readResolue
Не попадайте в зависимость от планировщика потоков
При выполнении в системе нескольких потоков соответствующий планировщик определяет, какие из них будут выполняться и в течение какого времени. Каждая правильная реализация JVМ пытается при этом добиться какой-то справедливости, однако конкретные стратегии диспетчеризации в различных реализациях сильно отличаются. Хорошо написанные многопоточные приложения не должны зависеть от особенностей этой стратегии. Любая программа, чья корректность или производительность зависит от планировщика потоков, скорее всего окажется не переносимой.
Лучший способ написать устойчивую, гибкую и переносимую многопоточную программу - обеспечить условия, при которых в любой момент времени может выполняться несколько потоков. В этом случае планировщику потоков остается совсем небольшой выбор: он лишь передает управление выполняемым потокам, пока те еще могут выполняться. Как следствие, поведение программы не будет сильно меняться даже при выборе совершенно других алгоритмов диспетчеризации потоков.
Основной прием, позволяющий сократить количество запущенных потоков, заключается в том, что каждый поток должен выполнять небольшую порцию работы, а затем ждать наступления некоего условия (используя Object.wait) либо истечения не которого интервала времени (используя Thгead.sleep). Потоки не должны находиться в состоянии активного ожидания (busy-wait), регулярно проверяя структуру данных и ожидая, пока с теми что-то произойдет. Помимо того, что программа при этом становится чувствительной к причудам планировщика, активное ожидание может значительно повысить нагрузку на процессор, соответственно уменьшая количество полезной работы, которую на той же машине могли бы выполнить остальные процессы.
Указанным рекомендациям отвечает пример с очередью заданий (статья 49): если предоставляемый клиентом метод pгocessItem имеет правильное поведение, то поток, обрабатывающий очередь, большую часть своего времени, пока очередь пуста, будет проводить в ожидании монитора. В качестве яркого примера того, как поступать не следует, рассмотрим еще одну неправильную реализацию класса WoгkQueue, в которой вместо работы с монитором используется активное ожидание:
//Ужасная программа: использует активное ожидание
// вместо метода Object.wait!
public abstract class WoгkQueue {
private final List queue = new LinkedList();
private boolean stopped = false;
191
import java.util.*;
public abstract class WorkQueue {
private final List queue = new LinkedList();
private boolean stopped = false;
protected WorkQueue() { new WorkerThread().start(); }
public final void enqueue(Object workItem) {
synchronized (queue) { queue.add(workItem); }
}
public final void stop() {
synchronized (queue) { stopped = true; }
}
protected abstract void processItem(Object workItem)
throws InterruptedException;
private class WorkerThread extends Thread {
public void run() {
final Object QUEUE_IS_EMPTY = new Object();
while (true) { // Главный цикл
Object workItem = QUEUE_IS_EMPTY;
synchronized (queue) {
if (stopped)
return;
if (!queue.isEmpty())
workItem = queue.remove(0);
}
if (workItem != QUEUE_IS_EMPTY) {
try {
processItem(workItem);
} catch (InterruptedException e) {
return;
}
}
}
}
}
}
Чтобы дать некоторое представление о цене, которую вам придется платить за такую реализацию, рассмотрим микротест, в котором создаются две очереди заданий и затем некое задание передается между ними в ту и другую сторону. (Запись о задании, передаваемая из одной очереди в другую,- это ссылка на первую очередь, которая служит адресом возврата.) Перед началом измерений программа выполняется десять секунд, 'чтобы система "разогрелась"; в течение следующих десяти секунд подсчитывается количество циклических переходов из очереди в очередь. На моей
192
машине окончательный вариант WorkQueue (статья 49) показал 23 000 циклических переходов в секунду, тогда как представленная выше некорректная реализация демонстрирует 17 переходов в секунду.
class PingPongQueue extends WorkQueue {
volatile int count = 0;
protected void processItem(final Object sender) {
count++;
WorkQueue recipient = (WorkQueue) sender;
recipient.enqueue(this);
}
}
public class WaitQueuePerf {
public static void main(String[] args) {
PingPongQueue q1 = new PingPongQueue();
PingPongQueue q2 = new PingPongQueue();
q1.enqueue(q2); // Запускаем систему
// Дадим системе 10 с на прогрев
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
}
// Подсчитаем количество переходов за 10 с
int count = q1.count;
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
}
System.out.println(q1.count - count);
q1.stop();
q2.stop();
}
}
Приведенная реализация WorkQueue может показаться немного надуманной, тем не менее нередко можно встретить многопоточные системы, в которых безо всякой необходимости запускается один или несколько лишних потоков. И хотя результат этого может быть не таким экстремальным, как здесь продемонстрировано, производительность и переносимость приложения, по-видимому, все же страдают.
193
Столкнувшись с тем, что программа едва работает из-за того, что некоторые потоки, по сравнению с остальными, не получают достаточно процессорного времени, не поддайтесь искушению “исправить" программу, добавив в нее вызовы Thread.yield. Вы можете заставить программу работать, однако полученное приложение не будет переносимым с точки зрения производительности. Вызовы yield, улучшающие производительность в одной реализации JVM, в другой ее ухудшают, а в третьей не оказывают никакого влияния. У Thread.yield нет строгой семантики. Лучше измените структуру приложения таким образом, чтобы сократить количество параллельно выполняемых потоков.
Схожий прием состоит в регулировании приоритетов потоков. приоритеты потоков числятся среди наименее переносимых характеристик платформы Java. Нельзя отрицать, что быстроту реагирования приложения можно настроить, отрегулировав приоритеты нескольких потоков, но необходимость в этом возникает редко, а полученные результаты будут меняться от одной реализации JVM к другой. Серьезную проблему живучести не решить с помощью приоритетов потоков. Проблема скорее всего вернется, пока вы не найдете и не устраните основную причину.
Метод Thread.yield следует использовать для того, чтобы искусственно увеличить степень распараллеливания программы на время тестирования. Благодаря просмотру большей части пространства состояний программы, это помогает; найти ошибки и удостовериться в правильности системы. Этот прием доказал свою высокую эффективность в выявлении скрытых ошибок многопоточной обработки.
Подведем итоги. Ваше приложение не должно зависеть от планировщика потоков. Иначе оно не будет ни устойчивым, ни переносимым. Как следствие, лучше не связывайтесь с методом Thread.yield и приоритетами. Эти функции предназначены единственно для планировщика. Их можно дозировано при менять для улучшения качества сервиса в уже работающей реализации, но ими нельзя пользоваться для "исправления" программы, которая едва работает.