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

简介: StringBuilder和StringBuffer的区别是面试的时候被提及最多的问题之一了,我们都知道stringbuffer是线程安全的,而stringbuilder不是线程安全的。通过stringbuffer和stringbuilder的源码,我们可以发现stringbuilder和stringbuffer都是继承了abstractstringbuilder这个

一、前言


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


public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
复制代码


public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
复制代码


public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
复制代码


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


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());
复制代码


最后的输出结果是


aebaf2ad2ce0433cbe44cd457bbc4ca9~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg


三、stringbuilder


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


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异常(数组索引越界异常)。


90062423fdc743a1b7f7185924c94ab8~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg


09d7f1361c5f45db94054e5efad9d055~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg


ce9d7422f0ae4d3fa8a0a43b780b7f1a~tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.webp.jpg


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


四、分析


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


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方法


public StringBuilder append(Object obj) {
        return append(String.valueOf(obj));
    public StringBuilder append(String str) {
        //调用的是AbstractStringBuilder的append的方法
        super.append(str);
复制代码


在看看父类abstractstringbuilder中的append方法


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方法


//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方法中有这样的两个个操作


ensureCapacityInternal(count + len);   //1
str.getChars(0, len, value, count);    //2
复制代码


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


* 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源码如下


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关键字修饰了,所以他能够保证线程同步和数据的准确性。

目录
相关文章
|
4月前
|
Java
【Java集合类面试十二】、HashMap为什么线程不安全?
HashMap在并发环境下执行put操作可能导致循环链表的形成,进而引起死循环,因而它是线程不安全的。
|
4月前
|
安全 算法 Java
【Java集合类面试二】、 Java中的容器,线程安全和线程不安全的分别有哪些?
这篇文章讨论了Java集合类的线程安全性,列举了线程不安全的集合类(如HashSet、ArrayList、HashMap)和线程安全的集合类(如Vector、Hashtable),同时介绍了Java 5之后提供的java.util.concurrent包中的高效并发集合类,如ConcurrentHashMap和CopyOnWriteArrayList。
【Java集合类面试二】、 Java中的容器,线程安全和线程不安全的分别有哪些?
|
4月前
|
存储 安全 Java
解锁Java并发编程奥秘:深入剖析Synchronized关键字的同步机制与实现原理,让多线程安全如磐石般稳固!
【8月更文挑战第4天】Java并发编程中,Synchronized关键字是确保多线程环境下数据一致性与线程安全的基础机制。它可通过修饰实例方法、静态方法或代码块来控制对共享资源的独占访问。Synchronized基于Java对象头中的监视器锁实现,通过MonitorEnter/MonitorExit指令管理锁的获取与释放。示例展示了如何使用Synchronized修饰方法以实现线程间的同步,避免数据竞争。掌握其原理对编写高效安全的多线程程序极为关键。
64 1
|
5月前
|
缓存 安全 Java
多线程线程池问题之为什么手动创建的线程池比使用Executors类提供的线程池更安全
多线程线程池问题之为什么手动创建的线程池比使用Executors类提供的线程池更安全
|
5月前
|
缓存 监控 Java
(十)深入理解Java并发编程之线程池、工作原理、复用原理及源码分析
深入理解Java并发编程之线程池、工作原理、复用原理及源码分析
|
5月前
|
设计模式 安全 NoSQL
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
71 0
|
5月前
|
安全 Java 调度
Java面试题:Java内存优化、多线程安全与并发框架实战,如何在Java应用中实现内存优化?在多线程环境下,如何保证数据的线程安全?使用Java并发工具包中的哪些工具可以帮助解决并发问题?
Java面试题:Java内存优化、多线程安全与并发框架实战,如何在Java应用中实现内存优化?在多线程环境下,如何保证数据的线程安全?使用Java并发工具包中的哪些工具可以帮助解决并发问题?
64 0
|
5月前
|
Java Redis 数据安全/隐私保护
Redis14----Redis的java客户端-jedis的连接池,jedis本身是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,最好用jedis连接池代替jedis,配置端口,密码
Redis14----Redis的java客户端-jedis的连接池,jedis本身是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,最好用jedis连接池代替jedis,配置端口,密码
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
48 1
C++ 多线程之初识多线程
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
20 3