Why StringBuilder Fails in Multithreaded Code and How StringBuffer Solves It

This article explains why StringBuilder is not thread‑safe, demonstrates the problem with a multithreaded example that produces a wrong length and occasional ArrayIndexOutOfBoundsException, and shows how StringBuffer’s synchronized implementation avoids these issues.

Programmer DD
Programmer DD
Programmer DD
Why StringBuilder Fails in Multithreaded Code and How StringBuffer Solves It

Introduction

During a recent interview I was asked about the difference between StringBuilder and StringBuffer and got stuck when the interviewer probed the thread‑safety aspect.

Interviewer: What is the difference between StringBuilder and StringBuffer? Me: StringBuilder is not thread‑safe, StringBuffer is thread‑safe. Interviewer: Where exactly is StringBuilder unsafe? Me: ... (stumped)

I only remembered the high‑level conclusion and never examined why StringBuilder is unsafe.

Analysis

StringBuilder, StringBuffer, and String share the same internal representation: a mutable char[] array. Unlike the immutable String whose array is final , the arrays in StringBuilder and StringBuffer can change.

To see the problem we run the following multithreaded code:

public class StringBuilderDemo {
    public static void main(String[] args) throws InterruptedException {
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 1000; j++) {
                        stringBuilder.append("a");
                    }
                }
            }).start();
        }
        Thread.sleep(100);
        System.out.println(stringBuilder.length());
    }
}

We expect the length to be 10 000, but the program often prints a smaller number (e.g., 9326) and may throw an ArrayIndexOutOfBoundsException.

1. Why the output differs from the expected value

StringBuilder (inherited from AbstractStringBuilder) contains two fields:

char[] value; // the character array
int count;   // number of used characters

The append() method ultimately executes:

public AbstractStringBuilder append(String str) {
    if (str == null) return appendNull();
    int len = str.length();
    ensureCapacityInternal(count + len);
    str.getChars(0, len, value, count);
    count += len;
    return this;
}

The statement count += len is not atomic. If two threads read the same count value simultaneously, both will write back the same incremented result, causing lost updates and a final length smaller than 10 000.

2. Why an ArrayIndexOutOfBoundsException may be thrown

The method ensureCapacityInternal() checks whether the current value array can accommodate the new characters and expands it if necessary:

private void ensureCapacityInternal(int minimumCapacity) {
    // overflow‑conscious code
    if (minimumCapacity - value.length > 0)
        expandCapacity(minimumCapacity);
}

Expansion creates a new array with roughly double the previous size plus two, then copies the old contents:

void expandCapacity(int minimumCapacity) {
    int newCapacity = value.length * 2 + 2;
    // ... some checks omitted
    value = Arrays.copyOf(value, newCapacity);
}

The copy operation uses System.arraycopy internally:

public static char[] copyOf(char[] original, int newLength) {
    char[] copy = new char[newLength];
    System.arraycopy(original, 0, copy, 0,
        Math.min(original.length, newLength));
    return copy;
}

When two threads interleave, one thread may finish the capacity check and increment count to 6, while the other thread still uses the old count value (5) in the subsequent str.getChars() call. The copy then writes beyond the current array bounds, triggering ArrayIndexOutOfBoundsException.

Thus the root cause of StringBuilder’s unsafety is the lack of atomicity in updating count and the non‑synchronized expansion of the internal buffer.

If we replace StringBuilder with StringBuffer in the same test, the program reliably prints 10 000 because StringBuffer synchronizes its mutating methods, guaranteeing thread safety.

Inspecting StringBuffer ’s append() implementation reveals the use of the synchronized keyword, which serializes access and prevents the race conditions described above.

END

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Javaconcurrencythread safetystringbuilderstringbuffer
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.