Why Is StringBuilder Not Thread‑Safe? Deep Dive into Java’s Internals
Although StringBuilder and StringBuffer share similar APIs, StringBuilder lacks thread safety because its append method updates shared fields without synchronization, leading to race conditions that can corrupt the internal char array and cause ArrayIndexOutOfBoundsException, as demonstrated by a multithreaded test example.
Reason Analysis
StringBuilder's append method does not use synchronized, while most methods in StringBuffer are declared with the synchronized keyword, providing method‑level locking.
public StringBuilder append(String str) {
super.append(str);
return this;
} public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}Exception Example
A test creates a shared StringBuilder, starts ten threads, each appending the character "a" 1,000 times. The expected final length is 10,000, but repeated runs often produce a smaller length and sometimes throw an ArrayIndexOutOfBoundsException.
@Test
public void test() throws InterruptedException {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
sb.append("a");
}
}).start();
}
// Sleep to ensure all threads finish
Thread.sleep(1000);
System.out.println(sb.length());
}Root Cause of Thread‑Unsafety
StringBuilder inherits two crucial fields from AbstractStringBuilder:
// fields in AbstractStringBuilder
char[] value;
int count;The append implementation increments count without any atomicity:
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; // non‑atomic update
return this;
}When two threads read the same count value, both add the length and write back the same result, causing lost updates and an incorrect final length.
Why the Exception Occurs
Because the stale count may prevent the internal array from expanding, ensureCapacityInternal can skip resizing, and the subsequent String.getChars copies beyond the actual array bounds, triggering the exception.
private void ensureCapacityInternal(int minimumCapacity) {
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value, newCapacity(minimumCapacity));
}
}The new capacity is calculated as twice the current size plus two:
private int newCapacity(int minCapacity) {
int newCapacity = (value.length << 1) + 2;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}Conclusion
Understanding that StringBuilder updates the shared count field without synchronization explains its lack of thread safety and why concurrent appends can produce wrong lengths or runtime exceptions. Use StringBuffer or external synchronization when mutable strings are accessed by multiple threads.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Senior Brother's Insights
A public account focused on workplace, career growth, team management, and self-improvement. The author is the writer of books including 'SpringBoot Technology Insider' and 'Drools 8 Rule Engine: Core Technology and Practice'.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
