Как чистые функции и мутабельность используются в многопоточности
Что проверяют
Связываете ли вы абстрактные понятия с моделью памяти Java. Интервьюер ждёт, что беда многопоточности в разделяемом изменяемом состоянии, а чистые функции и неизменяемые объекты его убирают. Следом обычно просят написать неизменяемый класс или спрашивают, достаточно ли final.
Как отвечать
Чистая функция вычисляет результат только из аргументов и ничего не меняет снаружи. Её можно вызывать из любого числа потоков без синхронизации: общего состояния, за которое они могли бы гоняться, нет. Неизменяемость даёт то же самое для данных:
finalи JMM: модель памяти гарантирует, что после конструктора все потоки видят корректные значенияfinalполей без синхронизации, если ссылка наthisне утекла из конструктора;record: удобный неизменяемый носитель данных, но неизменяемость у него поверхностная. Поле-список останется изменяемым, если не скопировать его черезList.copyOf;- коллекции:
List.ofиList.copyOfдают неизменяемые коллекции, аCollections.unmodifiableListлишь обёртку, исходный список под ней меняется; - Stream API:
parallelStreamбезопасен только с функциями без побочных эффектов.forEachс добавлением в общийArrayListдаёт гонку, результат собирают черезcollect.
Там, где изменяемое состояние неизбежно, его ограничивают: держат внутри одного потока или обновляют атомарно через AtomicReference.updateAndGet и ConcurrentHashMap.compute. Функция в updateAndGet должна быть без побочных эффектов: при конкуренции её вызовут повторно, и эффект сработает дважды. Для редко меняющихся списков с частым чтением подходит CopyOnWriteArrayList.
Цена неизменяемости: новый объект на каждое изменение, то есть нагрузка на GC на горячем пути.
Где спотыкаются
Считают, что final делает объект неизменяемым. final List<String> запрещает переприсвоить ссылку, но не добавить элемент в список.
Думают, что volatile решает гонку в count++. Он гарантирует видимость, а инкремент остаётся тремя операциями, нужен AtomicInteger или LongAdder.
Кладут в лямбду для parallelStream или updateAndGet запись в лог или во внешнюю коллекцию и потом ищут, откуда дубли и потери.