Back to Blog
Java

Java StringBuffer: Usage, Thread Safety, and Trade-offs

java stringbuffer: Understand Java StringBuffer: its purpose, core methods, thread-safety guarantees, and how it compares to StringBuilder for practical development.

StringBufferStringBuilderJava stringsthread safetyJava performance
Illustration of Java StringBuffer as a mutable character buffer with thread-safe operations, contrasting with immutable String.

Java's String is immutable, which means every concatenation creates a new object. When you build a string in a loop, the repeated allocation and copying can become a measurable bottleneck. java stringbuffer was introduced to solve that problem by providing a mutable sequence of characters that can grow without creating intermediate objects. It is part of the java.lang package and has been available since Java 1.0, making it one of the oldest mutable string classes in the language.

Why StringBuffer Exists: The Cost of String Concatenation

Consider a simple loop that appends numbers to a string:

String result = ""; for (int i = 0; i < 10000; i++) { result += i; }

Each += creates a new String, copies the existing content, and then appends the new characters. The time complexity is O(n²) because each iteration copies the entire accumulated string. For small loops this is irrelevant, but for large loops it becomes wasteful. StringBuffer avoids this by maintaining an internal array of characters and appending directly to it. The array grows automatically when needed, but the growth is amortized, similar to ArrayList.

Creating and Using StringBuffer

You can create a StringBuffer with an initial capacity or with an initial string:

StringBuffer buffer1 = new StringBuffer(); // default capacity 16 StringBuffer buffer2 = new StringBuffer(100); // initial capacity 100 StringBuffer buffer3 = new StringBuffer("hello"); // starts with "hello"

The capacity is not the length; it is the number of characters the buffer can hold before it needs to resize. If you know the approximate final size, providing an initial capacity avoids reallocation and copying.

To append content, use the append method, which is overloaded for all primitive types, String, char[], and Object:

StringBuffer buffer = new StringBuffer("Value: "); buffer.append(42); buffer.append(" "); buffer.append(true); System.out.println(buffer.toString()); // Value: 42 true

append returns the same StringBuffer instance, so you can chain calls:

StringBuffer buffer = new StringBuffer(); buffer.append("a").append("b").append("c");

Core Methods: Append, Insert, Delete, and Replace

Beyond append, StringBuffer provides a set of methods for manipulating the sequence. insert places characters at a specific index:

StringBuffer buffer = new StringBuffer("Java"); buffer.insert(4, " StringBuffer"); System.out.println(buffer.toString()); // Java StringBuffer

delete and deleteCharAt remove characters:

StringBuffer buffer = new StringBuffer("abcdef"); buffer.delete(1, 3); // removes "bc" System.out.println(buffer.toString()); // adef buffer.deleteCharAt(0); System.out.println(buffer.toString()); // def

replace replaces a range with a new string:

StringBuffer buffer = new StringBuffer("Hello World"); buffer.replace(6, 11, "Java"); System.out.println(buffer.toString()); // Hello Java

These methods all operate in place, which is the core advantage over immutable strings. They are straightforward to use, but note that insert and replace may require shifting the internal array, which is O(n) in the worst case.

Thread Safety and Synchronization

The most distinctive feature of StringBuffer is that its public methods are synchronized. That means each method call is atomic with respect to other synchronized methods on the same instance. If multiple threads share a single StringBuffer and each thread calls append without external locking, the operations are serialized, preventing data corruption.

StringBuffer sharedBuffer = new StringBuffer(); Runnable task = () -> { for (int i = 0; i < 1000; i++) { sharedBuffer.append("x"); } }; Thread t1 = new Thread(task); Thread t2 = new Thread(task); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(sharedBuffer.length()); // 2000

Without synchronization, two threads could interleave their writes and corrupt the internal state. The synchronized keyword on each method prevents that. However, this safety comes at a cost: acquiring and releasing a lock on every call adds overhead, even when the buffer is used by a single thread.

StringBuffer vs. StringBuilder: Performance Trade-offs

Java 5 introduced StringBuilder, which is identical to StringBuffer in API but does not synchronize its methods. For single-threaded usage, StringBuilder is faster because it avoids lock overhead. The difference is small for short strings but becomes measurable in tight loops with many operations.

AspectStringBufferStringBuilder
Thread safetySynchronized methodsNot synchronized
PerformanceSlightly slower due to lockingFaster in single-threaded use
Introduced inJava 1.0Java 5
APIIdentical core methodsIdentical core methods

In practice, the JVM can sometimes eliminate the lock if it detects no contention, but that is not guaranteed. The official Java documentation recommends using StringBuilder when possible, and StringBuffer only when thread safety is actually required.

Choosing Between StringBuffer and StringBuilder

Use StringBuilder unless you have a concrete requirement for thread safety. The typical scenario for StringBuffer is a shared buffer that is accessed by multiple threads without external coordination. For example, a logging utility where several threads append log lines to a common buffer before flushing.

public class LogBuffer { private final StringBuffer buffer = new StringBuffer(); public void log(String message) { buffer.append(System.currentTimeMillis()).append(": ").append(message).append('\n'); } public String flush() { String content = buffer.toString(); buffer.setLength(0); // clear without releasing capacity return content; } }

In this case, the synchronized methods prevent interleaved writes. But if you are building a string in a local variable or a method that is not shared, StringBuilder is the better choice. The performance difference is not huge, but it is unnecessary overhead to pay when you do not need it.

Common Mistakes and Misconceptions

One common mistake is using StringBuffer in a single-threaded context because the developer believes it is more modern or robust. It is not; it is simply older and slower. Another mistake is assuming that StringBuffer is safe for compound operations. While each individual method is synchronized, a sequence of calls is not atomic. For example:

if (buffer.length() == 0) { buffer.append("data"); }

Another thread could modify the buffer between the length() check and the append, leading to unexpected behavior. To make such a sequence atomic, you must synchronize on the buffer explicitly:

synchronized (buffer) { if (buffer.length() == 0) { buffer.append("data"); } }

Also, be aware that toString() returns a snapshot of the current content, not a live view. After calling toString(), subsequent modifications to the StringBuffer do not affect the returned String.

Memory and Capacity Management

StringBuffer grows its internal array automatically. The default growth strategy is to double the capacity when it is exceeded, but the exact behavior is implementation-dependent. If you know the final size, you can set the initial capacity to avoid reallocation. The ensureCapacity method lets you increase the capacity proactively:

StringBuffer buffer = new StringBuffer(); buffer.ensureCapacity(1000);

After building the string, you can call trimToSize() to reduce the capacity to the current length, which may free memory if the buffer is large and will be retained for a long time. However, trimToSize() is rarely needed because the buffer is usually short-lived. In high-memory environments, an oversized buffer that is kept in a cache can waste memory, so consider calling setLength(0) to clear it and reuse the same capacity, or let it be garbage-collected.

The choice between StringBuffer and StringBuilder is not about correctness in most cases; it is about matching the concurrency model. For new code, StringBuilder is the default. Use StringBuffer only when you have multiple threads sharing a mutable string and you want to avoid external synchronization. The synchronized methods give you a simple, safe building block, but they are not a substitute for atomic compound operations. Understanding the trade-off helps you write code that is both correct and efficient.

java stringbuffer: Practical Usage and Code Examples | RYUSLOG DEV