Question 6 · Section 12

Why StringBuffer is Slower than StringBuilder?

StringBuffer is slower because every one of its methods is marked with the synchronized keyword. This means before executing any operation (even a simple append), Java must acqu...

Language versions: English Russian Ukrainian

🟢 Junior Level

StringBuffer is slower because every one of its methods is marked with the synchronized keyword. This means before executing any operation (even a simple append), Java must acquire an object “monitor” (lock).

Simple analogy: Imagine a bathroom door:

  • StringBuilder — a door without a lock. Walk in and do your thing.
  • StringBuffer — a door with a lock. You need to unlock it first, walk in, do your thing, lock it back. Even if you’re alone in the building.

Example:

StringBuilder sb = new StringBuilder();  // No lock — fast
StringBuffer sb2 = new StringBuffer();   // With lock — slower

for (int i = 0; i < 1_000_000; i++) {
    sb.append("x");   // ~15ms
    sb2.append("x");  // ~25ms (60% slower)
}

// Numbers are approximate (JMH, single-thread, JDK 17, x86_64). // May differ on your hardware, but the ratio remains.

When this matters: Only when you call millions of operations. For normal tasks the difference is unnoticeable.


🟡 Middle Level

Why synchronized slows things down

Each synchronized method of StringBuffer requires:

  1. Monitor acquisition — checking if the object is locked by another thread
  2. Memory Barriers — processor synchronizes cache with main memory
  3. Monitor release — after method completion

Even if there’s only one thread, JVM still performs these steps.

Where it’s used in practice

StringBuffer is legacy from Java 1.0. In modern code it’s practically not used. The only scenario: one StringBuilder object is actually used from multiple threads (which itself is a rare and suspicious pattern).

Typical mistakes

  1. Mistake: Thinking StringBuffer is needed for “safety” Solution: If each thread uses its own buffer — StringBuilder is safe

  2. Mistake: Using StringBuffer for loop concatenation “just in case” Solution: Loop concatenation is almost always single-threaded — StringBuilder

Performance comparison (single-threaded mode)

| Operation | StringBuilder | StringBuffer | Difference | | ———- | ————– | ————– | ———- | | 1M append | ~15ms | ~25ms | +66% | | 1M insert | ~20ms | ~35ms | +75% | | 1M delete | ~10ms | ~18ms | +80% |


🔴 Senior Level

Internal Implementation

// StringBuffer — EVERY public method is synchronized
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

// StringBuilder — NOT A SINGLE synchronized method
public StringBuilder append(String str) {
    super.append(str);
    return this;
}

Both delegate to AbstractStringBuilder, but the StringBuffer wrapper adds synchronized on every call.

What’s behind synchronized

  1. Monitor Enter/Exit: JVM instructions monitorenter and monitorexit
  2. Memory Barriers: LoadLoad, StoreStore, LoadStore, StoreLoad — processor flushes and invalidates cache lines
  3. Object Header: Lock owner information is recorded in the object’s mark word

JVM optimizations: Lock Elision and Lock Coarsening

Lock Elision (lock removal):

void foo() {
    StringBuffer sb = new StringBuffer(); // Escape analysis: object doesn't "escape"
    sb.append("a");                       // JIT: synchronized can be removed!
    sb.append("b");
}

HotSpot via Escape Analysis can prove the object is not visible to other threads and remove synchronized.

Lock Coarsening (enlarging locks):

StringBuffer sb = ...;
sb.append("a");  // monitor acquisition
sb.append("b");  // monitor acquisition
sb.append("c");  // monitor acquisition

// JIT may combine into one lock:
// synchronized(sb) { append("a"); append("b"); append("c"); }

// JIT applies coarsening only if it sees sequential calls // within one compilable method. If calls are spread across different // methods — coarsening is impossible.

But: These optimizations are not guaranteed. They depend on:

  • -XX:+EliminateLocks (enabled by default)
  • C2 compilation budget
  • Method size (inlining budget)

Edge Cases

  1. Biased Locking (removed in Java 15): Previously JVM “biased” the monitor to the first thread, making subsequent captures free. Removed due to overhead in containers.

  2. Contended access: If 2+ threads actually compete for StringBuffer:
    • Thread A: BLOCKED → context switch → OS scheduling → resume
    • Context switch: ~1-10μs (much more expensive than monitor enter without contention)
  3. False Sharing: The StringBuffer object monitor may cause false sharing with neighboring objects in the cache line.

Performance (detailed benchmarks)

| Scenario | 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

Scenario: Logging in a web service (50K RPS):

  • StringBuffer for formatting each log: p99 latency = 8ms
  • StringBuilder: p99 latency = 5ms
  • Difference of 3ms × 50K = 150 CPU-seconds/sec → 150 cores wasted

Best Practices for Highload

  • StringBuilder — default choice
  • StringBufferonly if one buffer is actually shared between threads
  • If you need a thread-safe buffer, better to use StringBuilder + external synchronization (control over granularity)
  • For maximum performance: StringBuilder with initialCapacity + avoid reallocations

🎯 Interview Cheat Sheet

Must know:

  • StringBuffer is slower due to synchronized on every method
  • synchronized requires: monitorenter/monitorexit, memory barriers, monitor release
  • Even in single-threaded mode, JVM performs all synchronization steps
  • JVM may optimize via Lock Elision (Escape Analysis), but this is not guaranteed
  • Biased Locking removed in Java 15 — StringBuffer overhead in single-threaded mode is even higher
  • Contention (2+ threads): context switch ~1-10μs, much more expensive than monitor enter

Frequent follow-up questions:

  • How much slower is StringBuffer? — ~66% slower in single-threaded mode, ~1200%+ with 4-thread contention.
  • What is Lock Elision? — JIT via Escape Analysis proves the object is not visible to other threads and removes synchronized. Not guaranteed.
  • What is Lock Coarsening? — JIT combines sequential synchronized calls into one lock. Works only within one compilable method.
  • Why was Biased Locking removed? — overhead in containers. In Java 15+ StringBuffer overhead is even higher.

Red flags (DON’T say):

  • ❌ “StringBuffer is faster in single-threaded mode” — slower due to synchronized
  • ❌ “JVM always optimizes synchronized” — Lock Elision is not guaranteed
  • ❌ “StringBuffer is needed for every multithreaded program” — only if ONE buffer is shared
  • ❌ “The difference is unnoticeable in practice” — at 50K RPS: 3ms × 50K = 150 CPU-seconds/sec wasted

Related topics:

  • [[5. When to Use StringBuilder vs StringBuffer]]
  • [[4. Why String is Immutable]]