Перечислите принципы, которым вы следуете в многопоточном программировании
При написании многопоточных программ следует придерживаться определённых принципов, которые помогают обеспечить корректность, производительность и поддерживаемость кода.
1. Давайте значимые имена потокам.
OrderProcessor, QuoteProcessor значительно информативнее, чем Thread-1, Thread-2. При отладке, анализе дампов потоков и мониторинге это экономит часы.
Пример
Thread thread = new Thread(task, "order-processor-1");
// или
ExecutorService pool = Executors.newFixedThreadPool(4,
new ThreadFactoryBuilder().setNameFormat("order-worker-%d").build());
2. Минимизируйте область синхронизации. Блокировка затратна, переключение контекста ещё дороже. Синхронизируйте только критическую секцию минимального размера. Синхронизированный блок предпочтительнее синхронизированного метода.
3. Предпочитайте синхронизаторы вместо wait()/notify().
CountDownLatch, Semaphore, CyclicBarrier, Phaser, Exchanger — проще, надёжнее и поддерживаемее, чем ручное управление через wait()/notify().
4. Предпочитайте Concurrent Collections вместо Synchronized Collections.
ConcurrentHashMap масштабируется значительно лучше, чем Collections.synchronizedMap(). Concurrent-коллекции используют CAS, сегментную блокировку и copy-on-write — более тонкие механизмы, чем единый мьютекс.
5. Тщательно обрабатывайте прерывания.
InterruptedException — это сигнал потоку завершиться корректно. Его нельзя «проглатывать»:
Пример
// ПЛОХО
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// проглочено — поток не знает, что его прервали
}
// ХОРОШО
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // Восстанавливаем флаг
// Выполняем корректное завершение
}
6. Обрабатывайте исключения в потоках.
Пулы потоков «проглатывают» исключения из Runnable. Оборачивайте код в try-catch или используйте Callable с проверкой Future.get(). Устанавливайте Thread.UncaughtExceptionHandler для перехвата непойманных исключений.
7. Предпочитайте неизменяемые объекты. Immutable-объекты безопасны для многопоточного доступа без синхронизации. Чем больше общего состояния неизменяемо, тем проще и безопаснее код.
8. Используйте final поля для безопасной публикации.
JMM гарантирует, что значение final-поля будет видимо всем потокам после завершения конструктора. Это простейший способ безопасной публикации объекта.
9. Избегайте вложенных блокировок — это путь к deadlock. Если избежать невозможно, всегда захватывайте блокировки в одном и том же порядке во всех потоках.
10. Документируйте политику потокобезопасности.
Используйте аннотации @ThreadSafe, @NotThreadSafe, @GuardedBy (из JSR-305 / javax.annotation.concurrent) для явного документирования.
11. Для Java 21+: предпочитайте Virtual Threads для IO-bound задач, ReentrantLock вместо synchronized при длительных операциях (во избежание pinning), ScopedValue вместо ThreadLocal.
На собеседовании это открытый вопрос, который проверяет практический опыт. Перечислите 4-5 принципов с конкретными примерами из своего опыта, и объясните, какие проблемы они решают.