skip to content

A teammate proposes using StringBuffer as a shared field so several worker threads can append log lines to it concurrently. Critique this design.

level: principalimportance: should knowfreq 40%

answer

  1. sync = no corruption, NOT ordering
  2. one lock = contention bottleneck
  3. reading needs a done-boundary
  4. confine: per-thread builder + merge
  5. or ConcurrentLinkedQueue / logging lib

basics

~20 s

StringBuffer keeps each append from corrupting the buffer, but it does not control the order, makes every thread fight over one lock, and you still need extra synchronization to read it safely. A shared mutable builder is a bottleneck and a smell; give each thread its own builder or use a proper concurrent structure.

solid answer

~40 s

StringBuffer's synchronization only guarantees that an individual `append` won't corrupt the internal array; it says nothing about the *order* lines interleave, so the combined log is non-deterministic and probably not what you want. Worse, funnelling every worker through one lock serializes them on that hot field, killing the parallelism the design was meant to exploit and creating a contention bottleneck. Reading the result (e.g. `toString()`) while writers are active still needs careful synchronization or a clean handoff. The better designs are thread confinement — each worker builds its own StringBuilder and you merge results at the end — or a purpose-built concurrent structure like a `ConcurrentLinkedQueue<String>` (or a logging framework / `BlockingQueue` consumer) that is designed for many-producer access. StringBuffer's per-call locking is a 1990s convenience, not a concurrency design.

go deeper

for a junior

Recognizes StringBuffer is the thread-safe one, but likely accepts the design at face value.

for a middle

Spots that StringBuilder vs StringBuffer matters here and that locking has a cost, but may not articulate the ordering or contention problems fully.

for a senior

Identifies non-deterministic ordering, the single-lock contention bottleneck, and proposes thread confinement or a concurrent queue instead.

for a principal

Reframes the problem around not sharing mutable state at all, weighs throughput/back-pressure/ordering trade-offs, and recommends the right system-level solution (confinement, concurrent structures, or a logging framework) with rationale.

## The proposal and the hidden assumption The idea: one `StringBuffer` field, many threads each calling `append(line)`. The assumption is that because StringBuffer is 'thread-safe', this is correct and efficient. Both halves of that assumption are weak. ## What StringBuffer's thread safety actually buys **Synchronized** methods mean only one thread executes a given method on the object at a time, via an internal lock. So a single `append` call cannot interleave with another and corrupt the backing array — no torn writes, no lost characters within one call. That is *all* it guarantees. ## Problem 1: ordering is non-deterministic Thread safety per call is not the same as a meaningful global order. If thread A appends `"A: started\n"` and thread B appends `"B: started\n"`, the lock decides who goes first arbitrarily, run to run. For a log you usually want either timestamp ordering or at least per-line atomicity with a sensible interleave — neither is guaranteed. And if any writer does *two* appends to compose one line, those two can be split by another thread's append, interleaving partial lines. ## Problem 2: contention destroys parallelism The whole point of multiple workers is to do work in parallel. But every `append` now takes the *same* lock, so the threads serialize at exactly that point. Under load this single hot lock becomes a **contention bottleneck**: threads spend time waiting for the lock instead of working. You have added the cost of concurrency (context switches, lock handoffs) while removing its benefit at the shared point. ## Problem 3: safe reading is still unsolved When do you read the result? Calling `toString()` or `length()` while writers are mid-flight gives a snapshot of an in-progress buffer. Each call is synchronized, but you have no clean 'everyone is done' boundary unless you add one (e.g. a join/latch). So the design also needs external coordination it did not account for. ## Better designs 1. **Thread confinement (usually best).** Each worker owns a private `StringBuilder` (no synchronization, fast). When all finish, merge the pieces: ```java List<String> parts = workers.parallelStream() .map(w -> { StringBuilder local = new StringBuilder(); w.run(local); // appends only to its own builder return local.toString(); }) .toList(); String all = String.join("", parts); // one merge, no shared lock ``` No shared mutable state, full parallelism, deterministic merge order. 2. **A concurrent collection built for many producers.** e.g. each thread offers lines to a `ConcurrentLinkedQueue<String>` or a `BlockingQueue<String>` drained by a single consumer thread that writes them out. These structures are engineered for low-contention multi-producer access. 3. **A real logging framework.** For actual logging, libraries (Log4j2/SLF4J with async appenders) already solve ordering, batching, and back-pressure far better than a hand-rolled buffer. ## The principle Prefer **not sharing mutable state** over making shared mutable state thread-safe. StringBuffer's per-method lock is a low-level safety net, not a concurrency architecture; reaching for it signals that the real ownership/coordination question hasn't been answered. The senior-plus move is to redesign so the contention and ordering problems don't exist. ## Summary - Synchronized appends prevent corruption only, not ordering or sequence atomicity. - One shared lock serializes the workers — a contention bottleneck. - Reading the result still needs a completion boundary. - Prefer thread confinement (per-thread StringBuilder + merge) or a concurrent queue / logging framework.

  • Why doesn't StringBuffer's synchronization solve the ordering problem?
    It only serializes individual calls; which thread wins the lock first is arbitrary, so global line order is non-deterministic, and a logical line built from multiple appends can be split by another thread.
  • What is 'thread confinement' and why is it preferable here?
    Giving each thread its own un-shared mutable state (a private StringBuilder) so there is no contention and no synchronization needed; you merge the independent results at the end deterministically. No shared lock means full parallelism.

saying these in an interview costs you the question

  • Accepting 'StringBuffer is thread-safe so the design is fine.'
  • Ignoring that log line order becomes non-deterministic.
  • Missing the contention bottleneck from a single shared lock.
  • Not proposing thread confinement or a concurrent structure as the real fix.
  • Assuming per-call synchronization makes multi-append composition atomic.

context