JobPlus知识库 IT 软件开发 文章
为什么说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类型提高速度;


如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

¥ 打赏支持
404人赞 举报
分享到
用户评价(0)

暂无评价,你也可以发布评价哦:)

扫码APP

扫描使用APP

扫码使用

扫描使用小程序