为什么说StringBuilder线程不安全

一、前言

可能大家在学习java的基础过程中,都知道StringBuilder相对StringBuffer效率更高,但是线程不安全。可是,到底是怎么个不安全法,反正我是懵的。借此机会,写个小代码测试下。

二、编码

既然是测试StringBuilder和StringBuffer的线程安全性,那就分别new一个StringBuilder和StringBuffer作为共享变量,传递到多个线程进行操作,看看最后结果如何。

package com.cjt.test;

public class Test {

  public static void main(String[] args) {
    StringBuilder builder = new StringBuilder();
    StringBuffer buffer = new StringBuffer();

    for (int i = 0; i < 10; i++) {
      new A(builder, buffer).start();
    }
  }
}

class A extends Thread {

  private StringBuilder builder;

  private StringBuffer buffer;

  A(StringBuilder builder, StringBuffer buffer) {
    this.buffer = buffer;
    this.builder = builder;
  }

  @Override
  public void run() {
    for (int i = 0; i < 100; i++) {
      builder.append("c");
      buffer.append("c");

      try {
        Thread.sleep(10);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
    System.out.println("[" + Thread.currentThread().getName() + "]builder:" + builder.length());
    System.out.println("[" + Thread.currentThread().getName() + "]buffer:" + buffer.length());
  }
}

三、分析测试

我在run()里面加了一个Thread.sleep(10)延时,为了更好地体现多个线程操作同一个变量的安全性问题。运行结果:

[Thread-9]builder:939
[Thread-5]builder:939
[Thread-9]buffer:994
[Thread-5]buffer:994
[Thread-1]builder:941
[Thread-1]buffer:996
[Thread-4]builder:941
[Thread-4]buffer:996
[Thread-0]builder:941
[Thread-0]buffer:996
[Thread-8]builder:941
[Thread-8]buffer:996
[Thread-2]builder:942
[Thread-2]buffer:998
[Thread-6]builder:942
[Thread-6]buffer:998
[Thread-3]builder:944
[Thread-3]buffer:1000
[Thread-7]builder:944
[Thread-7]buffer:1000

说实话当我运行后也是一头问号?什么鬼,没有任何规律可寻。的确是这样,但是经过多次运行后可以总结一点是:StringBuffer最终的length总是1000,但是StringBuilder的length总是少于1000。

这也太没说服力了~!那就要分别看看它们的append()源码了;

StringBuilder:

public StringBuilder append(String str) {
    super.append(str);
    return this;
}

StringBuffer:

public synchronized StringBuffer append(String str) {
    super.append(str);
    return this;
}

看样子好像就隔只一个synchronized关键字,那就看看super.append(str)的源码吧,但可以吃惊地发现都是AbstractStringBuilder.append(String str),看看这段代码:

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

由于底层字符串都是由char数组实现的,这里str.getChars(0, len, value, count);就是一个明显的赋值操作。因而可以说StringBuilder和StringBuffer的append(String str)方法都是通过getChars方法来实现字符串拼接的

有人看到上面还有个ensureCapacityInternal(count + len);调用,这个只是一个底层的char数组扩容计算,有兴趣的可以自己私下看看源码,这里就不贴出来。

那我们就一探getChars方法的究竟:

public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
{
    if (srcBegin < 0)
        throw new StringIndexOutOfBoundsException(srcBegin);
    if ((srcEnd < 0) || (srcEnd > count))
        throw new StringIndexOutOfBoundsException(srcEnd);
    if (srcBegin > srcEnd)
        throw new StringIndexOutOfBoundsException("srcBegin > srcEnd");
    System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}

实质上是调用底层System.arraycopy()方法实现的,可能你觉得我扯远了,其实,的确有点吧。

StringBuilder中的append方法没有使用synchronized关键字,意味着多个线程可以同时访问这个方法。那么问题就来了额,如果两个线程同时访问到这个方法,那么AbstractStringBuilder中的count是不是就是相同的,所以这两个线程都是在底层char数组的count位置开始append添加,那么最终的结果肯定就是在后执行的那个线程append进去的数据会将前面一个覆盖掉。因此我们的控制台输出才会出现StringBuilder一直都是小于1000的。然而StringBuffer却不会发生这种情况。

三、总结

  1. StringBuilder相比StringBuffer效率更高,但多线程不安全;
  2. 在单线程中字符串的频繁拼接使用StringBuilder效率更高,对于多线程使用StringBuffer则更安全;
  3. 字符串简单操作时没必要使用上述两者,还是用String类型提高速度;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,778评论 18 399
  • java笔记第一天 == 和 equals ==比较的比较的是两个变量的值是否相等,对于引用型变量表示的是两个变量...
    jmychou阅读 1,525评论 0 3
  • 、6一、基本知识 1.JDK和JRE的区别 答:JDK是java语言开发工具包,包含JRE和开发工具(javac....
    梦游的沙师弟阅读 1,222评论 0 4
  • 集合框架: 1)特点:存储对象;长度可变;存储对象的类型可不同2)Collection(1)List:有序的;元素...
    Demo_Yang阅读 1,286评论 0 4
  • 韩版: 导演: 朴信宇 编剧: 东野圭吾 / 朴妍善 / 朴信宇 主演: 孙艺珍 / 韩石圭 / 高修 / 李珉廷...
    抽屉电影阅读 903评论 0 0