StringBuffer
StringBuffer is a mutable sequence of characters, just like StringBuilder, with one difference: its methods are synchronized, which makes each individual operation thread-safe. StringBuffer is the older of the two classes (it dates from Java 1.0; StringBuilder arrived in Java 5). Today it is used mainly in legacy code and in the rare case where one buffer is genuinely shared between threads.
Same API as StringBuilder
StringBuffer and StringBuilder have the same methods with the same behavior:
StringBuffer sb = new StringBuffer("Hello");
sb.append(", ").append("World").append('!'); // "Hello, World!"
sb.insert(0, ">> "); // ">> Hello, World!"
sb.replace(3, 8, "Hi"); // ">> Hi, World!"
sb.reverse(); // "!dlroW ,iH >>"
String result = sb.toString();
Everything from the StringBuilder lesson applies: append is overloaded for every type and returns the buffer for chaining; insert, delete, deleteCharAt, replace, setCharAt, reverse, and setLength edit in place; length, charAt, indexOf, and substring read; toString produces an independent String. Like StringBuilder, StringBuffer does not override equals, so compare contents with toString().equals(...), compareTo (Java 11+), or String.contentEquals.
What Synchronized Means
Each public method of StringBuffer acquires the object's lock before it runs and releases it afterward. When two threads call append on the same StringBuffer at the same time, one waits for the other to finish. The internal buffer therefore never ends up in a corrupted state.
StringBuilder has no locking. If several threads append to one StringBuilder simultaneously, their updates can interleave inside the method: two threads may write to the same position, characters can be lost, the length can end up wrong, or an ArrayIndexOutOfBoundsException can be thrown while the buffer is being resized. These failures are intermittent and depend on timing and the number of CPU cores, which makes them very hard to reproduce and debug.
Per-Method Safety Is Not Full Safety
Synchronization covers one method call at a time. A sequence of calls is not atomic:
StringBuffer log = new StringBuffer();
// Thread A and Thread B both run:
log.append("[").append(Thread.currentThread().getName()).append("] ").append(message).append('\n');
Each append is safe on its own, but another thread can append between them, so the pieces of two log lines can interleave:
[worker-1] [worker-2] Saved order
Sent email
To make a multi-step update atomic, hold the lock for the whole sequence, or build the complete line first and append it in a single call:
synchronized (log) {
log.append("[").append(name).append("] ").append(message).append('\n');
}
// or: build locally, then one atomic append
String line = "[" + name + "] " + message + "\n";
log.append(line);
This is a general lesson about thread safety: making individual operations synchronized does not make a combination of operations correct.
Performance
Acquiring a lock has a cost, even when no other thread is competing for it. Modern JVMs reduce this cost considerably, but StringBuilder is still somewhat faster for the overwhelmingly common case where a buffer is used by a single thread, and it is never slower.
StringBuilder or StringBuffer?
StringBuilder | StringBuffer | |
|---|---|---|
| Mutable | Yes | Yes |
| Methods synchronized | No | Yes |
| Safe to share between threads | No | Per method call only |
| Speed (single thread) | Faster | Slightly slower |
| Introduced | Java 5 | Java 1.0 |
| Recommended for new code | Yes | Only when sharing is truly needed |
The standard guidance is to use StringBuilder by default. Text builders are almost always local variables: created in a method, filled in, converted to a String, and discarded, all within one thread. Local variables are never shared, so synchronization would only add overhead.
StringBuffer remains relevant when:
- You maintain older code or call APIs that require it. For example,
java.util.regex.Matcher.appendReplacementandappendTailaccepted onlyStringBufferuntil Java 9 addedStringBuilderoverloads. - A single buffer really is appended to by several threads, and each append is a complete unit.
Even in multithreaded code there are usually better designs than a shared mutable buffer: let each thread build its own result and combine them at the end, collect results with a concurrent collection such as ConcurrentLinkedQueue, or use a logging framework, which handles concurrent writes itself.
Converting Between Them
StringBuilder builder = new StringBuilder("text");
StringBuffer buffer = new StringBuffer(builder); // constructor accepts any CharSequence
StringBuilder back = new StringBuilder(buffer);
Both implement CharSequence and Appendable, so code written against those interfaces works with either.
Practical Example: Concurrent Appends
The following program starts four threads that each append 100,000 characters to the same buffer, first with a StringBuffer and then with a StringBuilder, and compares the final lengths.
import java.util.ArrayList;
import java.util.List;
public class Buf {
static int run(Appendable target, int threads, int perThread) throws Exception {
List<Thread> workers = new ArrayList<>();
for (int t = 0; t < threads; t++) {
Thread th = new Thread(() -> {
try {
for (int i = 0; i < perThread; i++) target.append('x');
} catch (Exception e) {
System.out.println(" error: " + e.getClass().getSimpleName());
}
});
workers.add(th);
th.start();
}
for (Thread th : workers) th.join();
return ((CharSequence) target).length();
}
public static void main(String[] args) throws Exception {
int threads = 4, perThread = 100_000;
System.out.println("Expected length: " + threads * perThread);
System.out.println("StringBuffer length: " + run(new StringBuffer(), threads, perThread));
System.out.println("StringBuilder length: " + run(new StringBuilder(), threads, perThread));
}
}
Typical output on a multi-core machine:
Expected length: 400000
StringBuffer length: 400000
StringBuilder length: 371558
The StringBuffer result is always exactly 400,000, because each append holds the buffer's lock while it updates the length and writes the character. The StringBuilder result is unpredictable. On multi-core machines it is often smaller than expected because concurrent appends overwrite each other, and a run may also print an ArrayIndexOutOfBoundsException from a thread that wrote past the buffer while another thread was growing it. On a single-core machine, or by luck, it may even come out correct, which is exactly why such bugs slip through testing. The number shown above will differ on every run.
Both buffers are passed as Appendable, the interface that declares append, so the same method can drive either one. join makes the main thread wait for all workers before reading the length. Threads, join, and synchronization are covered in detail in the Java Multithreading section.