StringBuilder、StringBuffer的线程安全到底是什么

StringBuilder、StringBuffer的线程安全到底是什么

前言

昨天看到这个面试题:StringStringBuilderStringBuffer之间的区别是什么?

这个问题本身很简单:

  1. String是不可变的,对于字符串的拼接,都需要重新构造String对象
  2. StringBuilderStringBuffer主要解决String拼接字符串的问题,他们不需要重新构造对象
  3. StringBuilderStringBuffer的区别在于StringBuffer线程安全的

但是根据这个问题可以引申出一些其他问题

String为什么不可变

查看源码,StringStringBuilderStringBuffer内部都是使用char数组实现的。

String中,这个char数组是用final进行修饰的,所以一旦创建并不能改变其值(非常规方法是可以的)。

为什么要设计String不可变

JavaString保留在一个特殊的区域:字符串常量池。当创建一个String时,若在池中已经存在此字符串,则不会再创建,而是直接引用,若不存在,则创建。这是一种缓存策略。

为什么StringBuilder、StringBuffer可变

StringBuilderStringBuffer都继承自AbstractStringBuilder,其中的char数组并没有被final修饰。在append之类的方法中,进行了整个数组的复制,它们引用了新的char数组,原来的char数组相当于被抛弃了。

为什么StringBuffer是线程安全的

什么是线程安全

定义:多线程环境中,能永远保证程序的正确性

StringBuffer的线程安全到底是什么意思

StringBuilderStringBufferappend之类的方法中,执行了一系列的代码。

这个是AbstractStringBuilder源码

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

这一段代码大致意思是:计算新的数组的长度、将原来的数组内容复制到新的数组中,在新的数组中添加内容,增加长度,返回。

在多线程环境中,可能进行了多个append操作,导致在其中一个append的执行过程中,已经在执行其他的append操作。如:前一个append生成新的数组后,还未将内容添加到新的数组时,后面一个append又生成了新的数组,导致在新的数组中添加内容这一步,在同一个位置添加了两次,若添加的内容超过新的数组长度,会抛出StringIndexOutOfBoundsException

StringBuilder中,并没有对append进行处理,所以它仍然是按照AbstractStringBuilder的逻辑执行。

但是在StringBuffer中,重写了所有的append之类的操作,添加了synchronized关键字,保证了这一段代码的原子性,保证了线程安全。

测试代码

import java.util.Locale;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class Main {

    private static final Random seedRandom = new Random(4L);

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

        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 50; i++) &#123;
            executor.execute(() -> &#123;
                Random random = new Random(seedRandom.nextLong());
                builder.append(random.nextInt(10));
                buffer.append(random.nextInt(10));
            &#125;);
        &#125;

        TimeUnit.SECONDS.sleep(1);
        System.out.println(builder);
        System.out.println(builder.length());

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

        executor.shutdown();
    &#125;
&#125;

多执行这段代码几次,StringBuffer得到的结果长度会总是50,而StringBuilder偶尔会少于50次,这就是线程不安全的情况下出现的错误。

我的疑问

这样的线程安全有什么用,说实话,没有用过StringBuffer,一般的需求都是在单线程进行字符串的拼接。

我的疑问是:多线程使用StringBuffer怎么能够保证拼接的顺序呢?

我在使用过程中,对字符串拼接对顺序是有要求的,并不希望因为线程原因导致错误。而且字符串拼接的线程安全性,通常可以使用其他方法来解决。

总结:线程安全 != 保证拼接顺序

StringBuffer的线程安全只是保证在一次append操作中,不会有其他append介入,避免了拼接的过程中出现异常。

若在多线程环境中,StringBuffer并不像队列一般,能够将拼接按特定的顺序执行。


   转载规则


《StringBuilder、StringBuffer的线程安全到底是什么》 Mycroft Wong 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
okhttp系列 okhttp系列
OkHttp架构分析
OkHttp是一个非常好用的网络数据库,本系列文章以OkHttp 3.14.2版本,也是最后一个Java版本
下一篇 
Android包优化个人总结 Android包优化个人总结
Android包优化个人总结前言打包apk对app运行本身会有很多用处,减少代码量,减少资源量,减少资源名等,都可以减少apk的大小。如果apk足够大,会发现,release包比debug包启动都会快很多。 所以优化非常有必要,同时为了安全
  目录