skip to content

What is the core difference between StringBuilder and StringBuffer, and which should you reach for by default?

level: middleimportance: must knowfreq 85%

answer

  1. same API, one difference: synchronized
  2. Buffer = sync (slow, 1.0); Builder = no sync (fast, Java 5)
  3. default StringBuilder
  4. Buffer only for shared mutable instance
  5. per-call lock != atomic sequence

basics

~20 s

They have the same API and do the same job: build strings in a mutable buffer. StringBuffer's methods are synchronized (thread-safe but slower); StringBuilder's are not (faster). Use StringBuilder by default; use StringBuffer only when one instance is shared across threads.

solid answer

~40 s

StringBuilder and StringBuffer expose virtually identical APIs (`append`, `insert`, `delete`, `reverse`, `toString`) and both wrap a growable `char[]`. The single meaningful difference is thread safety: StringBuffer (Java 1.0) marks its mutating methods `synchronized`, so concurrent calls on the same instance are safe but pay locking cost on every call; StringBuilder (Java 5) drops synchronization for speed. In practice almost all string building happens inside one method on one thread, where the lock is pure overhead, so StringBuilder is the correct default. StringBuffer is justified only when a single builder instance is genuinely shared and mutated by multiple threads — which is rare; usually you would instead confine the builder to one thread or use higher-level coordination. Even StringBuffer's per-call locking does not make a *sequence* of operations atomic.

go deeper

for a junior

Knows StringBuffer is thread-safe and StringBuilder is not, and that StringBuilder is the usual choice.

for a middle

Explains synchronized methods, the performance trade-off, the version history (1.0 vs Java 5), and confidently defaults to StringBuilder with the rationale.

for a senior

Adds the atomicity-vs-thread-safety nuance, notes that shared mutable builders are usually a design smell, and discusses thread confinement as the better pattern.

for a principal

Reasons about API/ABI compatibility (the CharSequence/AbstractStringBuilder hierarchy), the cost of legacy synchronized APIs, and steering teams away from sharing mutable builders entirely.

## Same job, same shape Both classes solve the same problem: efficiently assembling a String without creating a new object per step (see the immutability/loop discussion). Internally each holds a resizable **`char[]`** (a character array) and a length counter, and `append` writes into the free space, doubling the array when it fills. Their public methods are essentially the same: `append`, `insert`, `delete`, `replace`, `reverse`, `setLength`, `charAt`, `toString`. You can usually swap one for the other by changing only the type name. ## The one real difference: synchronization **Synchronized** is a Java keyword that puts a lock around a method: only one thread can be executing any synchronized method on that object at a time. This makes individual method calls **thread-safe** — safe to call from multiple threads at once without corrupting the internal array — but acquiring and releasing the lock costs CPU time on *every* call, even when only one thread is involved. - **StringBuffer** (introduced in Java 1.0): all mutating methods are `synchronized`. Thread-safe per call, but slower. - **StringBuilder** (introduced in Java 5): identical API with **no** synchronization. Not thread-safe, but faster. Because Java 5 added StringBuilder specifically to remove the unnecessary locking, the guidance flipped: **default to StringBuilder**. ## Why StringBuilder is the default The overwhelmingly common pattern is to create a builder as a local variable inside one method, append to it, and return `toString()` — all on a single thread: ```java String render(List<String> rows) { StringBuilder sb = new StringBuilder(); for (String r : rows) sb.append(r).append('\n'); return sb.toString(); } ``` Here no other thread can ever touch `sb` (it never escapes the method), so StringBuffer's locks would buy nothing and cost time. StringBuilder is strictly better. ## When StringBuffer is actually justified Only when a *single* builder instance is shared as mutable state across threads — e.g. a field that several threads append to. This is rare and usually a design smell; cleaner alternatives are to give each thread its own builder, or to guard a shared one with your own higher-level lock. ## The atomicity trap StringBuffer's synchronization makes each *individual* call atomic, but NOT a *sequence* of calls. Two threads each doing `sb.append("a"); sb.append("b");` can interleave to produce `"abab"` or `"aabb"`. So per-method synchronization rarely gives the correctness you actually want — another reason it is seldom the right tool. ## Summary - Same API, same internal growing buffer. - StringBuffer = synchronized (thread-safe per call, slower); StringBuilder = unsynchronized (faster). - Default to StringBuilder; StringBuffer only for a genuinely shared mutable instance. - Even StringBuffer does not make multi-call sequences atomic.

  • If StringBuffer is thread-safe, why is it not the default?
    Because nearly all string building is single-threaded and local, so the per-call lock is pure overhead. StringBuilder removes that cost. Synchronization should be paid only when there is real shared mutable state, which is rare for builders.
  • Does StringBuffer guarantee correct results if two threads append to the same instance?
    Each call won't corrupt the buffer, but the interleaving of multiple calls is unspecified, so the resulting text order is non-deterministic. Per-method synchronization is not the same as making your whole logical operation atomic.

saying these in an interview costs you the question

  • Saying StringBuilder is thread-safe — it is not.
  • Recommending StringBuffer 'to be safe' in ordinary single-threaded code.
  • Believing StringBuffer makes a sequence of appends atomic across threads.
  • Thinking the two differ in API or in how the buffer grows — only synchronization differs.

context