Почему StringBuffer потокобезопасен и в чем его разница с другими классами для строк в Java
Что проверяют
Понимаете ли вы, откуда берётся потокобезопасность у каждого из трёх классов, и различаете ли два разных её источника: неизменяемость у String и синхронизацию у StringBuffer. Следом обычно спрашивают, почему по умолчанию всё равно берут StringBuilder.
Как отвечать
Коротко по классам:
Stringнеизменяем. Любая операция возвращает новый объект, поэтому делить строку между потоками безопасно без всякой синхронизации, а литералы к тому же лежат в общем пуле;StringBuilderизменяемый буфер поверх массива, без синхронизации, выбор по умолчанию;StringBufferтот же буфер, но публичные методы помеченыsynchronized, то есть захватывают монитор самого объекта.
Полезно добавить внутреннюю деталь: оба изменяемых класса наследуют общую реализацию с массивом и ёмкостью, StringBuffer лишь переопределяет методы с синхронизацией. При нехватке места массив расширяется с копированием, поэтому начальную ёмкость иногда задают явно.
И главная оговорка: синхронизирован отдельный метод, а не последовательность. Такой код остаётся гонкой:
if (sb.length() > 0) {
sb.append(", ");
}Между проверкой и вызовом успевает вклиниться другой поток. Атомарность куска целиком даёт только внешняя блокировка.
Где спотыкаются
Ставят знак равенства между «методы синхронизированы» и «код потокобезопасен». Инвариант из нескольких операций защищает лишь внешняя блокировка, и это классический уточняющий вопрос после такого ответа.
Второе: берут StringBuffer в локальной переменной на всякий случай. Объект не покидает метод, делить его не с кем, а стоимость синхронизации остаётся и в коде, и в голове читателя.
Третье: склеивают строки через + в цикле, считая, что компилятор всё оптимизирует. На каждой итерации собирается новый буфер и копируется накопленный результат, на длинных данных это растёт квадратично. Вне цикла конкатенация нормальна.