Чому 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 vs StringBuffer]]
- [[4. Чому String є незмінним]]