Java:字符序列:为什么说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类型提高速度;

原文参考:https://www.jianshu.com/p/2cf1f60b0d9a

你可能感兴趣的:(java)