StringBuilder为什么线程不安全【源码分析】 一、

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

作者的其他平台:

| CSDN:blog.csdn.net/qq_4115394…

| 掘金:juejin.cn/user/651387…

| 知乎:www.zhihu.com/people/1024…

| GitHub:github.com/JiangXia-10…

| 公众号:1024笔记

本文大概1770字,读完共需11分钟

一、前言

StringBuilder和StringBuffer的区别是面试的时候被提及最多的问题之一了,我们都知道stringbuffer是线程安全的,而stringbuilder不是线程安全的。通过stringbuffer和stringbuilder的源码,我们可以发现stringbuilder和stringbuffer都是继承了abstractstringbuilder这个抽象累,然后实现了Serializable, CharSequence接口。其次stringbuilder和stringbuffer的内部实现其实跟String是一样的,都是通过一个char类型的数组进行存储字符串的,但是是String类中的char数组是final修饰的,是不可变的,而StringBuilder和StringBuffer中的char数组没有被final修饰,是可变的。

1
2
3
4
5
java复制代码public final class StringBuilder

extends AbstractStringBuilder

implements java.io.Serializable, CharSequence
1
2
3
4
5
java复制代码public final class StringBuffer

extends AbstractStringBuilder

implements java.io.Serializable, CharSequence
1
2
3
4
5
6
7
arduino复制代码public final class String

implements java.io.Serializable, Comparable<String>, CharSequence {

/** The value is used for character storage. */

private final char value[];
1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码abstract class AbstractStringBuilder implements Appendable, CharSequence {

* The value is used for character storage.

//stringbuilder和stringbuffer都继承了AbstractStringBuilder 但AbstractStringBuilder 中的

//char数组没有使用final修饰,这就是为什么string是不可变,但stringbuffer和stringbuilder是可变的

* The count is the number of characters used.

* This no-arg constructor is necessary for serialization of subclasses.

那么为什么stringbuilder和stringbuffer一个是线程安全一个不是的呢?如果在多线程中分别使用stringbuilder和stringbuffer会是什么样呢?

二、stringbuffer

首先来看看stringbuffer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码    public static void main(String[] args) throws InterruptedException {

StringBuffer sb = new StringBuffer();

for(int i=0;i<10;i++){

new Thread(new Runnable() {

@Override

public void run() {

for(int j=0;j<10000;j++){

sb.append("嗯");

//线程休眠300毫秒,这里要抛出异常

Thread.sleep(300);

//输出sb的长度是多少,理论上来说最后应该输出100000

System.out.println(sb.length());

最后的输出结果是

三、stringbuilder

与理论值一样。接下来再看看使用stringbuilder。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码    public static void main(String[] args) throws InterruptedException {

StringBuilder sb = new StringBuilder();

for(int i=0;i<10;i++){

new Thread(new Runnable(){

@Override

public void run() {

for(int j=0;j<10000;j++){

sb.append("嗯");

Thread.sleep(300);

System.out.println(sb.length());

理论上来说结果应该跟stringbuffer一样输出100000,但是实际结果是85560与预期结果不一样,而且多执行几次,每次结果也不一样(都小于预期值)(stringbuffer执行多次结果都一样),而且有时候会抛ArrayIndexOutOfBoundsException异常(数组索引越界异常)。

所以我们可以发现在多线程中使用stringbuilder确实是线程不安全的。为什么实际的输出值不对呢?

四、分析

前面提到过因为stringbuffer和stringbuilder都是继承了AbstractStringBuilder,在AbstractStringBuilder中我们可以看到定义了一个char数组和一个count变量

1
2
3
4
5
kotlin复制代码abstract class AbstractStringBuilder implements Appendable, CharSequence {

* The value is used for character storage.

* The count is the number of characters used.

另外stringbuilder和stringbuffer通过append方法来进行字符串的增加,我们先看看stringbuilder中append方法

1
2
3
4
5
6
7
8
9
typescript复制代码    public StringBuilder append(Object obj) {

return append(String.valueOf(obj));

public StringBuilder append(String str) {

//调用的是AbstractStringBuilder的append的方法

super.append(str);

在看看父类abstractstringbuilder中的append方法

1
2
3
4
5
6
7
8
9
python复制代码public AbstractStringBuilder append(String str) {

return appendNull();

int len = str.length();

ensureCapacityInternal(count + len);

str.getChars(0, len, value, count);

在多线程编程中有个重要的概念是叫原子操作,原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有切换到任何的一个其他的线程)。上述代码中的count+=len就不是一个原子操作,它等同于count=count+len,比如在上诉代码中,执行到count的值为99998的时候,新建一个len长度为1,但是当有两个线程同时执行到了count+=len的时候,他们的count的值都是99998,然后分别各自都执行了count+=len,则执行完之后的值都是99999,然后将值赋给count,则count最后的结果是99999,不是正确的100000,所以在多线程中执行stringbuilder的值始终会小于正确的结果。

但是stringbuilder和stringbuffer都是继承了abstractstringbuilder为什么结果不一样呢。既然abstractstringbuilder中的append方法肯定都是一样的我们再来看看stringbuffer中的append方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typescript复制代码    //append操作被synchronized 关键字修饰了

public synchronized StringBuffer append(Object obj) {

toStringCache = null;

super.append(String.valueOf(obj));

//append操作被synchronized 关键字修饰了

public synchronized StringBuffer append(String str) {

toStringCache = null;

super.append(str);

可以发现stringbuffer中的append操作被synchronized关键字修饰了。这个关键字肯定不会陌生,主要用来保证多线程中的线程同步和保证数据的准确性。所以再多线程中使用stringbuffer是线程安全的。

再AbstractStringBuilder的append方法中有这样的两个个操作

1
2
3
scss复制代码ensureCapacityInternal(count + len);   //1

str.getChars(0, len, value, count); //2

转到第一个操作方法的源码,可以发现这是一个是检查StringBuilder对象的原r数组的大小是否能装下新的字符串的方法,如果装不下了就new一个新的数组,新的数组的容量是原来char数组的两倍,再通过CopyOf()方法将原数组的内容复制到新数组.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码     * For positive values of {@code minimumCapacity}, this method

* behaves like {@code ensureCapacity}, however it is never

* If {@code minimumCapacity} is non positive due to numeric

* overflow, this method throws {@code OutOfMemoryError}.

private void ensureCapacityInternal(int minimumCapacity) {

// overflow-conscious code

if (minimumCapacity - value.length > 0) {

value = Arrays.copyOf(value,

newCapacity(minimumCapacity));

然后第二步操作是将String对象里面char数组里面的内容拷贝到StringBuilder对象的char数组里面。getchars源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
arduino复制代码   public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {

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);

可以看到原来在这里会抛出StringIndexOutOfBoundsException的异常。接着分析:

假设之前的代码中有两个线程a和b同时执行了append方法,并且都执行完了ensureCapacityInternal()方法,此刻count=99997,如果当线程a执行完了,则轮到线程2继续执行,线程b执行完了append方法之后,count变成了99998,这个时候如果线程a执行到了上面的getchars方法的时候他得到的count的值就是99998,这个时候就会抛ArrayIndexOutOfBoundsException的异常了。

五、总结

stringbuilder是线程不安全的,因为stringbuilder继承了父类abstractstringbuilder的append方法,该方法中有一个count+=len的操作不是原子操作,所以在多线程中采用stringbuilder会丢失数据的准确性并且会抛ArrayIndexOutOfBoundsException的异常。

stringbuffer是线程安全的因为他的append方法被synchronized关键字修饰了,所以他能够保证线程同步和数据的准确性。

往期精彩回顾

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%