W3docs

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, чтобы сохранить группу записей вместе.

java— editable, runs on the server

Посмотрите на вывод программы, и вы увидите два паттерна. Маркеры [T?:i] сохраняются целыми — без разрывов тегов — поскольку каждый из них является единственным вызовом append. Но порядок, в котором появляются маркеры разных потоков, перемежается. Три строки -- end of T? --, напротив, каждая попадает в буфер подряд, потому что внешний блок synchronized (shared) { ... } удерживает блокировку на протяжении всех четырёх вызовов append для этой группы.

Что дальше

Это завершает рассмотрение сборки изменяемых строк в обоих вариантах. Следующая тема — формирование строк для отображения: вывод чисел с фиксированной шириной, форматирование дат, выравнивание столбцов. Переходите к форматированию строк в Java.

Практика

Практика
Какое утверждение наиболее точно описывает разницу между `StringBuffer` и `StringBuilder`?
Какое утверждение наиболее точно описывает разницу между `StringBuffer` и `StringBuilder`?
Was this page helpful?