May 31, 2021 Article blog
The article comes from the public number: Program New Horizons Author: Ugly Fat Man Ii Brother
In the previous interview questions, we compared the differences between String String, StringBuilder, and StringBuffer, one of which mentioned that StringBuilder is non-threaded, so what causes StringBuilder's threads to be unsafe?
If you look at the source code of
StringBuilder
or
StringBuffer
you'll say that because
StringBuilder
doesn't use thread synchronization when it comes to
append
operations,
StringBuffer
uses the
synchronized
keyword for method-level synchronization in almost all methods.
This statement is certainly true, as can be seen by comparing some of the source code of
StringBuilder
and
StringBuffer
StringBuilder
append
method source code:
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
StringBuffer
append
method source code:
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
There is certainly nothing wrong with the above conclusion, but it does not explain what
StringBuilder
thread to be unsafe? W
hy use
synchronized
to keep threads safe?
What would happen if it wasn't used?
Let's go through it one by one.
Let's start with a code sample to see if the results are in line with our expectations.
@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();
}
// 睡眠确保所有线程都执行完
Thread.sleep(1000);
System.out.println(sb.length());
}
The above business logic is relatively simple, that is, to build a
StringBuilder
and then create 10 threads, each thread stitching string "a" 1000 times, theoretically when the thread execution is complete, the result of printing should be 10000 is right.
But the result of doing the above code printing multiple times is that the probability of 10000 is very small, in most cases less than 10000. At the same time, there is a certain probability of the following abnormal information"
Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException
at java.lang.System.arraycopy(Native Method)
at java.lang.String.getChars(String.java:826)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:449)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at com.secbro2.strings.StringBuilderTest.lambda$test$0(StringBuilderTest.java:18)
at java.lang.Thread.run(Thread.java:748)
9007
Processing strings in
StringBuilder
relies primarily on two member
char
array
value
and
count
StringBuilder
completes the append operation of the string by increasing the volume of
value
and the corresponding addition of
count
// 存储的字符串(通常情况一部分为字符串内容,一部分为默认值)
char[] value;
// 数组已经使用数量
int count;
Both of these properties are located in its abstract parent
AbstractStringBuilder
If you look at the construction method, we'll see that the initial length of the array
value
is set when
StringBuilder
is created.
public StringBuilder(String str) {
super(str.length() + 16);
append(str);
}
The default is the incoming string length plus 16.
This is what
count
exists because part of the array is the default.
When the
append
method is called,
count
is increased, and the added value is the length of the
append
string, and the implementation is also in the abstract parent class.
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 point where thread insecurity occurs is the "plus" operation of
count
in the
append
method. W
e know that the operation is thread-insecure, and then two threads read
count
value of 5 at the same time, and after the plus-1 operation, they become 6 instead of the expected 7.
Once this happens, the expected results do not occur.
Looking back at the stack information for the exception, there is one line:
at java.lang.String.getChars(String.java:826)
The corresponding code is the code in the
append
method in
AbstractStringBuilder
above.
The source code for the corresponding method is as follows:
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > value.length) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
In fact, the exception occurs at the bottom of
JVM
when the last line of
arraycopy
occurs.
The core operation of
arraycopy
is to
copy
the incoming
String
object into
value
And the reason for the abnormal occurrence is clear
value
of the subscript only to 6, but the program to access and operate the position marked 7, of course, run abnormal.
So why go beyond that position? T
his has to do with the less addition of
count
as we mentioned above. B
efore you perform the
str.getChars
method, you also need to check whether the current
value
is used according to
count
and if it is finished, expand the capacity.
The corresponding methods in
append
are as follows:
ensureCapacityInternal(count + len);
The specific implementation of ensureCapacityInternal:
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
count
should have been 7,
value
length 6, and should have triggered expansion. H
owever, because concurring
count
to be 6, assuming
len
is 1,
minimumCapacity
passed is 7 and does not expand capacity.
This causes the
str.getChars
method to visit a non-existent location later when it performs a replication operation, and therefore throws an exception.
Here's a drop-in look at the
newCapacity
approach in the expansion method:
private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value.length << 1) + 2;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
In addition to the check section, the core is to double the length of the new array and add 2.
The calculated new length is expanded as a parameter of
Arrays.copyOf
After the above analysis, do you really understand why
StringBuilder
threads are unsafe?
In the process of learning and practicing, we should not only know some conclusions, but also know the underlying principles of these conclusions, and more importantly, learn how to analyze the underlying principles.
Above is
W3Cschool编程狮
about
why StringBuilder is thread-unsafe?
Related to the introduction, I hope to help you.