skip to content

In Kotlin/JVM, how does StringBuilder relate to StringBuffer, and how should you handle text building that is shared across threads?

level: seniorimportance: nice to knowfreq 25%

answer

  1. Same API; StringBuffer synchronized, StringBuilder not
  2. Kotlin StringBuilder = typealias to java.lang.StringBuilder (JVM)
  3. Sharing a builder unsynchronized = data race
  4. StringBuffer's per-method locks don't make compound ops atomic
  5. Prefer confinement (per-thread builder) or Mutex over the whole build

basics

~20 s

StringBuilder is fast but not thread-safe. StringBuffer is the older, synchronized (thread-safe) version but slower. For shared mutable text, prefer giving each thread its own builder or using proper synchronization rather than relying on StringBuffer.

solid answer

~40 s

On Kotlin/JVM, `StringBuilder` and `java.lang.StringBuffer` share the same `AbstractStringBuilder` API (`append`, `insert`, `deleteAt`). The only difference is that `StringBuffer`'s methods are `synchronized` (thread-safe), while `StringBuilder` is not synchronized and is therefore faster for the overwhelmingly common single-threaded case. Sharing a single `StringBuilder` across threads without external synchronization is a data race — you can get corrupted output, lost appends, or `ArrayIndexOutOfBoundsException`. The right approach is almost never to swap in `StringBuffer`: per-method synchronization is coarse and you usually need the whole build atomic anyway. Prefer **confinement** — each thread/coroutine builds its own local `StringBuilder` (or `buildString`) and you merge results — or guard a shared builder with a `Mutex`/lock for the entire critical section. Kotlin's `StringBuilder` is a `typealias` to `java.lang.StringBuilder` on JVM; on Kotlin Multiplatform it maps to the platform implementation.

code

kotlin · 11 lines
kotlin
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

class Log {
    private val sb = StringBuilder()
    private val mutex = Mutex()
    suspend fun line(s: String) = mutex.withLock {
        sb.append(s).append('\n')   // whole critical section guarded
    }
    suspend fun dump(): String = mutex.withLock { sb.toString() }
}

go deeper

for a junior

Knows StringBuilder is not thread-safe and StringBuffer is the synchronized variant.

for a middle

Explains the synchronized-vs-not distinction and that sharing a StringBuilder across threads is unsafe.

for a senior

Argues why StringBuffer rarely solves the real problem (compound non-atomicity), and applies confinement or whole-section locking; knows the typealias detail.

for a principal

Designs concurrent text-building around immutable confinement and merge, reasons about JMM guarantees, lock granularity, and avoids legacy synchronized types in modern Kotlin/coroutine code.

## Same API, different synchronization On the JVM both `StringBuilder` and `StringBuffer` extend `AbstractStringBuilder`, so they expose the same mutating methods (`append`, `insert`, `deleteAt`, `replace`, `setLength`, …). The difference: - **`StringBuffer`**: every mutating method is `synchronized` on the buffer instance — individually thread-safe, but slower due to lock acquisition. - **`StringBuilder`**: no synchronization — faster, intended for single-threaded building. Kotlin's `kotlin.text.StringBuilder` is a `typealias` for `java.lang.StringBuilder` on JVM (and maps to the platform type on other Kotlin targets). `buildString` always uses `StringBuilder`. ## Why "just use StringBuffer" is usually wrong Even though each `StringBuffer` method is atomic, **compound** operations are not. Consider: ```kotlin if (sb.length < limit) sb.append(part) // check-then-act: NOT atomic ``` Between the `length` read and the `append`, another thread can mutate the buffer. So per-method locking rarely matches the granularity you need — you almost always want the whole logical build to be atomic, which `StringBuffer` does not give you for free. You pay for locks you don't benefit from. ## Sharing a StringBuilder is a data race Mutating one `StringBuilder` from multiple threads without synchronization is undefined behavior under the Java Memory Model. Symptoms: interleaved/garbled text, dropped characters, or `ArrayIndexOutOfBoundsException` when one thread resizes while another indexes. ## The right patterns 1. **Confinement (preferred)** — give each thread/coroutine its own builder, then combine: ```kotlin val parts = items.parallelStream() .map { buildString { append(render(it)) } } // local builder per task .toList() val result = parts.joinToString("") ``` 2. **Explicit synchronization** when a builder truly must be shared — guard the whole critical section, not single calls: ```kotlin val mutex = Mutex() suspend fun add(part: String) = mutex.withLock { sb.append(part) } ``` (Or a `synchronized(lock) { ... }` block / `ReentrantLock` for non-coroutine code.) ## Summary guidance - Default to `StringBuilder` / `buildString`. - Don't reach for `StringBuffer` as a thread-safety fix — it's legacy and its locking granularity rarely matches your needs. - For concurrency, **confine then merge**, or lock the whole build. Thread-confinement is the cleanest and fastest.

  • If StringBuffer is thread-safe, why not always use it?
    Its per-method synchronization adds overhead even single-threaded and still doesn't make compound check-then-act builds atomic, so you usually need your own locking anyway. It's legacy.
  • What's the cleanest concurrency pattern for building text?
    Thread confinement: each task builds its own local StringBuilder/buildString, then merge with joinToString — no shared mutable state, no locks.

StringBuffer locks the pen for each letter you write; but if two people need to write a whole sentence each, you really want them on separate sheets (confinement), not fighting over one pen per letter.

saying these in an interview costs you the question

  • Believing StringBuilder is thread-safe
  • Recommending StringBuffer as the go-to concurrency fix
  • Thinking per-method synchronization makes compound builds atomic
  • Sharing one StringBuilder across threads without any lock
  • Not knowing Kotlin's StringBuilder is the JVM StringBuilder

context