Почему StringBuffer медленнее StringBuilder?
StringBuffer медленнее потому, что каждый его метод помечен ключевым словом synchronized. Это означает, что перед выполнением любой операции (даже простого append) Java должна з...
🟢 Junior Level
StringBuffer медленнее потому, что каждый его метод помечен ключевым словом synchronized. Это означает, что перед выполнением любой операции (даже простого append) Java должна захватить “монитор” (блокировку) объекта.
Простая аналогия: Представьте дверь в туалет:
StringBuilder— дверь без замка. Зашёл и сделал дело.StringBuffer— дверь с замком. Нужно сначала открыть замок, зайти, сделать дело, закрыть замок. Даже если вы один в здании.
Пример:
StringBuilder sb = new StringBuilder(); // Без замка — быстро
StringBuffer sb2 = new StringBuffer(); // С замком — медленнее
for (int i = 0; i < 1_000_000; i++) {
sb.append("x"); // ~15ms
sb2.append("x"); // ~25ms (на 60% медленнее)
}
// Цифры примерные (JMH, single-thread, JDK 17, x86_64). // На вашем железе могут отличаться, но соотношение сохранится.
Когда это важно: Только когда вы вызываете миллионы операций. Для обычных задач разница незаметна.
🟡 Middle Level
Почему synchronized замедляет
Каждый synchronized-метод StringBuffer требует:
- Захват монитора — проверка, не занят ли объект другим потоком
- Memory Barriers — процессор синхронизирует кэш с основной памятью
- Освобождение монитора — после завершения метода
Даже если поток всего один, JVM всё равно выполняет эти шаги.
Где используется на практике
StringBuffer — legacy из Java 1.0. В современном коде его практически не используют. Единственный сценарий: один StringBuilder-объект реально используется из нескольких потоков (что само по себе — редкий и подозрительный паттерн).
Типичные ошибки
-
Ошибка: Думать, что
StringBufferнужен для “безопасности” Решение: Если каждый поток использует свой буфер —StringBuilderбезопасен -
Ошибка: Использовать
StringBufferдля конкатенации в цикле “на всякий случай” Решение: Конкатенация в цикле почти всегда однопоточная —StringBuilder
Сравнение производительности (однопоточный режим)
| Операция | StringBuilder | StringBuffer | Разница | | ———- | ————– | ————– | ——— | | 1M append | ~15ms | ~25ms | +66% | | 1M insert | ~20ms | ~35ms | +75% | | 1M delete | ~10ms | ~18ms | +80% |
🔴 Senior Level
Internal Implementation
// StringBuffer — КАЖДЫЙ публичный метод синхронизирован
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
// StringBuilder — НИ ОДНОГО synchronized
public StringBuilder append(String str) {
super.append(str);
return this;
}
Оба делегируют в AbstractStringBuilder, но обёртка StringBuffer добавляет synchronized на каждый вызов.
Что стоит за synchronized
- Monitor Enter/Exit: JVM-инструкции
monitorenterиmonitorexit - Memory Barriers: LoadLoad, StoreStore, LoadStore, StoreLoad — процессор сбрасывает и инвалидирует кэш-линии
- Object Header: В mark word объекта записывается информация о владельце блокировки
JVM оптимизации: Lock Elision и Lock Coarsening
Lock Elision (устранение блокировок):
void foo() {
StringBuffer sb = new StringBuffer(); // Escape analysis: объект не "убегает"
sb.append("a"); // JIT: synchronized можно убрать!
sb.append("b");
}
HotSpot через Escape Analysis может доказать, что объект не виден другим потокам, и убрать synchronized.
Lock Coarsening (укрупнение блокировок):
StringBuffer sb = ...;
sb.append("a"); // захват монитора
sb.append("b"); // захват монитора
sb.append("c"); // захват монитора
// JIT может объединить в одну блокировку:
// synchronized(sb) { append("a"); append("b"); append("c"); }
// JIT применяет coarsening только если видит последовательные вызовы // в пределах одного compilable метода. Если вызовы разнесены по разным // методам — коарсенинг невозможен.
Но: Эти оптимизации не гарантированы. Они зависят от:
-XX:+EliminateLocks(включён по умолчанию)- Компиляционного бюджета C2
- Размера метода (inlining budget)
Edge Cases
-
Biased Locking (удалён в Java 15): Раньше JVM “привязывала” монитор к первому потоку, делая последующие захваты бесплатными. Удалён из-за overhead в контейнерах.
- Contended доступ: Если 2+ потока реально конкурируют за
StringBuffer:- Thread A: BLOCKED → context switch → OS scheduling → resume
- Context switch: ~1-10μs (гораздо дороже monitor enter без contention)
- False Sharing: Монитор объекта
StringBufferможет вызвать false sharing с соседними объектами в кэш-линии.
Производительность (детальные бенчмарки)
| Сценарий | StringBuilder | StringBuffer | Delta | | ———————— | ————– | ————– | ———- | | 1 thread, no escape | 15ms | 18ms (elision) | +20% | | 1 thread, escapes | 15ms | 25ms | +66% | | 4 threads, no contention | 15ms x4 | 25ms x4 | +66% | | 4 threads, contention | 15ms x4 | 200ms | +1233% |
Production Experience
Сценарий: Логирование в веб-сервисе (50K RPS):
StringBufferдля форматирования каждого лога: p99 latency = 8msStringBuilder: p99 latency = 5ms- Разница в 3ms × 50K = 150 CPU-секунд/сек → 150 ядер впустую
Best Practices для Highload
StringBuilder— дефолтный выборStringBuffer— только если один буфер реально шарится между потоками- Если нужен thread-safe буфер, лучше использовать
StringBuilder+ внешнюю синхронизацию (контроль над granularity) - Для максимальной производительности:
StringBuilderсinitialCapacity+ avoid reallocations
🎯 Шпаргалка для интервью
Обязательно знать:
StringBufferмедленнее из-заsynchronizedна каждом методе- synchronized требует: monitorenter/monitorexit, memory barriers, освобождение монитора
- Даже в однопоточном режиме JVM выполняет все шаги синхронизации
- JVM может оптимизировать через Lock Elision (Escape Analysis), но это не гарантировано
- Biased Locking удалён в Java 15 — overhead
StringBufferв однопоточном режиме ещё выше - Contention (2+ потока): context switch ~1-10μs, гораздо дороже monitor enter
Частые уточняющие вопросы:
- Насколько
StringBufferмедленнее? — ~66% медленнее в однопоточном режиме, ~1200%+ при contention 4 потоков. - Что такое Lock Elision? — JIT через Escape Analysis доказывает, что объект не виден другим потокам, и убирает
synchronized. Не гарантировано. - Что такое Lock Coarsening? — JIT объединяет последовательные synchronized-вызовы в одну блокировку. Работает только в пределах одного компилируемого метода.
- Почему Biased Locking удалили? — overhead в контейнерах. В Java 15+ overhead
StringBufferещё выше.
Красные флаги (НЕ говорить):
- ❌ “
StringBufferбыстрее в однопоточном режиме” — медленнее из-за synchronized - ❌ “JVM всегда оптимизирует synchronized” — Lock Elision не гарантирован
- ❌ “
StringBufferнужен для каждой многопоточной программы” — только если ОДИН буфер шарится - ❌ “Разница незаметна на практике” — при 50K RPS: 3ms × 50K = 150 CPU-секунд/сек впустую
Связанные темы:
- [[5. Когда использовать StringBuilder, а когда StringBuffer]]
- [[4. Почему String является иммутабельным]]