StringBuffer

Su Suriya Ravichandran Updated 14 Nov 2026
5 min read ·Lesson 101 of 133

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?

StringBuilderStringBuffer
MutableYesYes
Methods synchronizedNoYes
Safe to share between threadsNoPer method call only
Speed (single thread)FasterSlightly slower
IntroducedJava 5Java 1.0
Recommended for new codeYesOnly 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.appendReplacement and appendTail accepted only StringBuffer until Java 9 added StringBuilder overloads.
  • 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.

0 Comments

Reviewed before they appear

No comments yet.

Java
Java Introduction Java Features Java Syntax Java Comments Java Statements Java Output Java Variables Java Data Types Java Type Casting Java Operators Java Strings Java Numbers Java Math Java Booleans Java User Input Java Conditions Java If Else Java Switch Java While Loop Java For Loop Java Break Java Continue Java Arrays Java Methods Java Parameters Java Return Values Java Scope Java Recursion Install Java Install JDK Java IDEs Java JDK Java JRE Java JVM Java Compiler Java File Compile Java Run Java JShell Basics Java Packages Java OOP Java Classes Java Objects Class Methods Constructors Constructor Overload this Keyword static Keyword final Keyword Encapsulation Inheritance Polymorphism Abstraction Access Modifiers super Keyword Method Overload Method Override Instanceof Operator Nested Classes Inner Classes Anonymous Classes Enums Records Array Basics Array Length Loop Arrays Multidimensional Arrays Array Copy Array Sorting Array Search ArrayList LinkedList HashSet TreeSet HashMap TreeMap Queue Interface Deque Interface Stack Class Iterator List Interface Set Interface Map Interface Collections Class Comparable Comparator Generics Basics Generic Classes Generic Methods Wildcards String Basics String Length String Methods String Concatenation String Compare String Search String Replace String Split String Format StringBuilder StringBuffer Text Blocks Regular Expressions Number Types Integer Class Float Class Double Class BigInteger BigDecimal Math Class Random Numbers Number Format Date Basics LocalDate LocalTime LocalDateTime ZonedDateTime Date Formatting Date Parsing Period Class Duration Class Java Instant Class Java Exception Basics Java try...catch Java Multiple Catch Blocks Java finally Block Java throw Keyword Java throws Keyword Java Checked Exceptions Java Unchecked Exceptions Java Custom Exceptions Java try-with-resources Java Exception Chaining
Ask about this post
AI Ask about this post

Ask questions about StringBuffer and get answers drawn from it.

Signed-in readers only.