Как создавать потоки и работать с синхронизацией в Java
Что проверяют
Не синтаксис запуска потока, а понимание модели памяти Java: что именно даёт synchronized, чем volatile отличается от атомарности и почему ручной new Thread() в современном коде почти не встречается.
Как отвечать
Создание. Задачу описывают через Runnable или Callable, а не наследованием от Thread: наследование расходует единственный слот наследования и намертво связывает задачу со способом её запуска. В реальном коде потоками управляет пул:
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> f = pool.submit(() -> compute());
pool.shutdown();С Java 21 для блокирующих задач есть виртуальные потоки: Executors.newVirtualThreadPerTaskExecutor(), их можно заводить сотнями тысяч. На CPU-нагрузке выигрыша они не дают, там по-прежнему пул размером с число ядер.
Синхронизация решает две задачи сразу: взаимное исключение и видимость (отношение happens-before). count++ это чтение, сложение и запись, поэтому без защиты обновления теряются.
class Counter {
private int count = 0;
public synchronized void increment() { count++; }
public synchronized int get() { return count; } // читатель тоже под замком
}Геттер без synchronized может сколь угодно долго возвращать устаревшее значение: это ошибка видимости, а не гонка за запись, и она не лечится «более быстрым» кодом.
Инструменты по ситуации:
volatileдаёт видимость и запрет переупорядочивания, но не атомарность:volatile int c; c++по-прежнему ломается. Подходит для флагаrunning;AtomicIntegerдля счётчика без блокировок,LongAdderлучше под высокой конкуренцией на запись;ReentrantLockтам, где нужныtryLock(timeout), прерываемое ожидание, справедливость или несколько объектовCondition. Освобождение обязательно вfinally;- готовые конкурентные структуры вместо самодельных локов:
ConcurrentHashMap,BlockingQueue,CopyOnWriteArrayList,CountDownLatch,Semaphore; - взаимная блокировка лечится единым порядком захвата замков, минимальными критическими секциями и
tryLockс таймаутом. Диагностика:jcmd <pid> Thread.print, он прямо пишет про найденный deadlock.
Ещё пара вещей, которые любят спросить: thread.run() вместо thread.start() выполняет задачу в текущем потоке и никакой параллельности не даёт; исключение внутри задачи в Future не теряется, а всплывает при get() в виде ExecutionException, тогда как у execute(Runnable) оно уходит в UncaughtExceptionHandler.
Где спотыкаются
Синхронизируют только запись, оставляя геттер открытым, а плавающие значения потом списывают на «особенности JIT» вместо признания проблемы видимости.