Как определить, что класс не потокобезопасен
Что проверяют
Умеете ли вы читать код глазами многопоточности: видеть разделяемое изменяемое состояние, составные операции и проблемы видимости. Следом обычно дают конкретный класс, просят найти гонку и исправить её минимальными средствами.
Как отвечать
Первый вопрос: есть ли у объекта изменяемое состояние, доступное нескольким потокам. Класс без полей или неизменяемый (все поля final, ссылки наружу не утекают) безопасен сразу. Если состояние есть, ищите признаки:
- составные операции без синхронизации:
count++это чтение, сложение и запись, и два потока теряют инкременты. То же с «проверить и сделать»:if (!map.containsKey(k)) map.put(k, v); - видимость: поле меняется в одном потоке и читается в другом без
volatile,synchronizedили атомиков. Без отношения happens-before другой поток может вечно видеть старое значение; - утечка внутреннего состояния: геттер отдаёт наружу внутренний
ArrayListилиHashMap, и его меняют без всякого контроля; - небезопасная публикация:
thisуходит наружу из конструктора, ленивая инициализация через double-checked locking безvolatile; - общие статические поля: статический
SimpleDateFormatилиHashMap, один на все потоки; - документация: в Javadoc у
ArrayList,HashMap,StringBuilderпрямо сказано, что синхронизации нет.
Одних глаз мало. SpotBugs ловит часть случаев, например несогласованную синхронизацию полей, а jcstress гоняет код в разных чередованиях потоков.
Исправление по ситуации: AtomicInteger или LongAdder для счётчиков, ConcurrentHashMap с computeIfAbsent и merge, неизменяемые объекты, synchronized или ReentrantLock для инвариантов из нескольких полей.
Где спотыкаются
Считают, что volatile чинит count++. Он даёт видимость, но не атомарность.
Думают, что ConcurrentHashMap или Collections.synchronizedList делают безопасным любой код вокруг них. Отдельные вызовы атомарны, а связка get и put уже нет, как и итерация по синхронизированному списку без внешней блокировки.
Доказывают потокобезопасность тем, что «тест прошёл сто раз». Гонки проявляются под нагрузкой и на другом железе.