Java StringBuffer
Используйте потокобезопасный класс StringBuffer в Java для изменяемых строк, разделяемых между потоками.
StringBuffer — старший брат StringBuilder. У них общий API, общий родитель (AbstractStringBuilder) и общая реализация на основе буфера байтов. Единственное отличие в том, что методы StringBuffer являются synchronized — каждый вызов append, insert, delete и toString захватывает монитор буфера на время выполнения. Это делает StringBuffer безопасным для использования в нескольких потоках. Это также делает каждую операцию медленнее аналогичной операции в StringBuilder.
Изначально стандартная библиотека Java содержала только StringBuffer. StringBuilder появился в JDK 1.5 (2004) именно потому, что накладные расходы на синхронизацию стали проблемой в распространённом однопоточном случае. Последние два десятилетия StringBuilder является выбором по умолчанию, а StringBuffer — нишевым решением.
Когда его стоит использовать
Начнём с честного резюме: почти никогда. Список реальных случаев применения невелик, и в большинстве из них в современной Java есть лучшие решения.
StringBuffer — правильный выбор только тогда, когда все следующие условия выполняются одновременно:
- Один буфер действительно используется несколькими потоками совместно.
- Каждый поток самостоятельно добавляет данные или вставляет их в буфер.
- На выходе потребителю нужна единственная неизменяемая строка
String. - Нет возможности легко переструктурировать код так, чтобы каждый поток строил собственный
StringBuilder, а координатор объединял их в конце.
В большинстве конкурентных программ последний пункт даёт выход: дайте каждому потоку свой StringBuilder, верните строки и объедините их в конце. Это устраняет конкуренцию за единственный монитор и убирает накладные расходы на синхронизацию с горячего пути.
Оставшийся случай — небольшое количество потоков, пишущих в общий диагностический буфер, или журнал аудита, в который добавляют данные несколько участников, — это то место, где StringBuffer ещё оправдывает своё существование.
API совпадает с StringBuilder
Каждый мутирующий метод StringBuilder присутствует в StringBuffer с той же сигнатурой и тем же возвращаемым типом. Поскольку оба класса расширяют AbstractStringBuilder, они являются поведенческими близнецами; единственное отличие — блокировка:
StringBuffer sb = new StringBuffer(64);
sb.append("Hello, ")
.append("world")
.insert(0, "[INFO] ")
.append('!');
String out = sb.toString();Конструкторы, length(), capacity(), charAt, substring, indexOf, reverse, delete, replace, setLength, ensureCapacity, trimToSize — всё присутствует, всё возвращает те же типы. Код-ревью для StringBuffer — это по сути то же самое, что для StringBuilder, плюс заметка о том, какой монитор удерживается.
Что означает синхронизация в данном случае
synchronized на методах экземпляра блокирует сам объект-буфер. Таким образом:
StringBuffer log = new StringBuffer();
// Thread A
log.append("hit /users\n");
// Thread B (concurrently)
log.append("hit /orders\n");Каждый вызов append выполняется атомарно по отношению к другому — ни один поток не затронет байты другого. Результатом будет "hit /users\nhit /orders\n" или "hit /orders\nhit /users\n". Но не "hit /uhit /ordserss\n\n". Именно это гарантируется.
Гарантия не распространяется на несколько вызовов методов подряд. Последовательность из двух append не является атомарной:
log.append(level); // unlock
// ← another thread might append here
log.append(": ");
log.append(message);
log.append('\n');Второй поток может вставить запись между первыми двумя вызовами append и перемешать данные. Если нужно, чтобы целая запись попала в буфер подряд, сначала соберите её в thread-local StringBuilder, а затем добавьте готовую строку одним вызовом append к общему StringBuffer. Или — эквивалентно — оберните многошаговую запись в явный блок synchronized (log) { ... }.
Производительность, кратко
Каждый заблокированный вызов платит за монитор: в современном HotSpot это дёшево на пути biased-lock-fast-path при отсутствии конкуренции и заметно дороже при её наличии. По сравнению с StringBuilder один неконкурентный вызов append медленнее лишь на незначительный постоянный множитель. При конкуренции разница резкая, поскольку потоки блокируются в ожидании монитора.
Вывод: стоимость определяется конкуренцией, а не самим примитивом блокировки. Если вы обнаружили горячий и конкурентный StringBuffer, правильное решение — переструктурировать код так, чтобы каждый поток строил свою часть, а не дальнейшая настройка буфера.
Практический пример
Программа ниже запускает небольшой пул потоков-писателей, которые совместно используют один StringBuffer и добавляют строки-маркеры. Синхронизированные методы сохраняют каждую строку целой; намеренная двухшаговая запись демонстрирует, почему иногда всё равно нужен внешний блок synchronized, чтобы сохранить группу записей вместе.
Посмотрите на вывод программы, и вы увидите два паттерна. Маркеры [T?:i] сохраняются целыми — без разрывов тегов — поскольку каждый из них является единственным вызовом append. Но порядок, в котором появляются маркеры разных потоков, перемежается. Три строки -- end of T? --, напротив, каждая попадает в буфер подряд, потому что внешний блок synchronized (shared) { ... } удерживает блокировку на протяжении всех четырёх вызовов append для этой группы.
Что дальше
Это завершает рассмотрение сборки изменяемых строк в обоих вариантах. Следующая тема — формирование строк для отображения: вывод чисел с фиксированной шириной, форматирование дат, выравнивание столбцов. Переходите к форматированию строк в Java.