Как сделать переменную потокобезопасной, если она изменяется несколькими потоками
Что проверяют
Различаете ли вы видимость и атомарность и умеете ли подобрать инструмент под конкретную операцию, а не отвечать «поставлю synchronized». Следом почти всегда спрашивают, почему volatile int со count++ не спасает.
Как отвечать
Сначала уточните, какая операция выполняется над переменной. От этого зависит выбор:
volatile: гарантирует видимость и порядок, запись в одном потоке видна чтению в другом через отношение happens-before. Подходит для флага вродеrunningили ссылки, которую только перезаписывают целиком. Составные операции он атомарными не делает;AtomicInteger,AtomicLong,AtomicReference: атомарные операции на CAS без блокировок. КромеincrementAndGetестьcompareAndSet,updateAndGetиaccumulateAndGetдля произвольного обновления;LongAdder: счётчик под высокой конкуренцией, раскладывает инкременты по ячейкам и заметно быстрееAtomicLong, когда пишут много потоков, а читают редко;synchronizedилиReentrantLock: когда инвариант держится на нескольких переменных сразу.ReentrantLockдобавляетtryLockс таймаутом, прерываемое ожидание и условия черезnewCondition;- без общей изменяемости: неизменяемые объекты,
ThreadLocal,ConcurrentHashMapсmergeиcomputeвместо ручной синхронизации вокругHashMap.
Про виртуальные потоки стоит сказать отдельно: в Java 21 блокирующий вызов внутри synchronized закреплял виртуальный поток за несущим (pinning). В JDK 24 это исправили, но в проектах на 21 для таких мест по-прежнему выбирают ReentrantLock.
Где спотыкаются
Считают volatile достаточным для count++. Это три шага: чтение, сложение, запись, и два потока спокойно теряют инкремент.
Защищают запись замком, а читают без него. Без общего замка или volatile читающий поток может не увидеть последнее значение, модель памяти Java тут ничего не обещает.
Собирают атомарные вызовы в составную операцию: if (count.get() < limit) count.incrementAndGet(). Каждый вызов атомарен, а связка нет, и лимит будет превышен. Нужен updateAndGet или цикл с compareAndSet.