笔记之String、StringBuilder、StringBuffer

1.安全性

  • StringBuilder是线程不安全的,String、StringBuffer是线程安全性。

为什么这么说呢?

        代码如下:

import java.util.concurrent.TimeUnit;

/**
 * @author ql
 * @version 1.0 2021/4/23
 */
public class StringDemo {
    public static void main(String[] args) {
        StringBuilder sb=new StringBuilder();
        StringBuffer sbf=new StringBuffer();

        for (int i = 0; i < 10; i++) {
         new Thread(()->{
             for (int j = 0; j < 10000; j++) {
                 sb.append("a");
                 sbf.append("a");
             }
         }).start();
        }
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("StringBuilder添加后的长度:"+sb.length());
        System.out.println("StringBuffer添加后的长度:"+sbf.length());
    }
}

运行结果
StringBuilder可能会爆的异常:Exception in thread "Thread-1" 
java.lang.ArrayIndexOutOfBoundsException

StringBuilder添加后的长度:99709
StringBuffer添加后的长度:100000

        在上述的运行结果中,我们能看到这段代码创建了10个线程,每个线程循环10000次分别往StringBuilder、StringBuffer对象里面append字符。正常情况下代码应该输出100000的。但是为什么只有StringBuffer的对象的长度为100000,而StringBuilder的长度小于10000,并且StringBuilder还抛出了一个ArrayIndexOutOfBoundsException异常(数组索引超出范围异常 )(不过这个异常不是一定会出现的,我试了几次才出现)。

1.1 为什么输出值跟预期值不一样

        我们先看一下StringBuilder、StringBuffer的两个成员变量(这两个成员变量实际上是定义在AbstractStringBuilder里面的,StringBuilder和StringBuffer都继承了AbstractStringBuilder)

 public final class StringBuffer extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence

public final class StringBuilder extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence

再看StringBuffer的append()方法:

    @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

再看StringBuilder的append()方法:

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

StringBuffer、StringBuilder的append()方法都调用的父类AbstractStringBuilder的append()方法;

    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;
    }
  • 这个时候我们直接看第七行,count += len明显不是一个原子操作。假设这个时候count值为10,len值为1,两个线程同时执行到了第七行,拿到的count值都是10,执行完加法运算后将结果赋值给count,所以两个线程执行完后count值为11,而不是12。这就是为什么测试代码输出的值要比10000小的原因。
  • 而为什么StringBuffer同样也是调用了父类AbstractStringBuilder的append()方法,但是输出的结果正常呢?在于StringBuffer的append方法添加锁synchronized ,这样就导致了调用父类AbstractStringBuilder的append()方法的时候,也会添加锁,同一时间只有一个线程可以进行字符串的添加操作,所以最后的结果正常。

1.2 存储的方式

        由下面的代码可以看到String存储数据的底层是char 数组,并且其是被final修饰,表示不可变的,而因为StringBuilder,StringBuffer都是继承AbstractStringBuilder,所以其存储数据的底层也是char 数组,但是,其没有被final修饰,所以StringBuilder,StringBuffer都是可变的。

String中的

//存储字符串的具体内容
private final char value[];

AbstractStringBuilder中:

//存储字符串的具体内容
char[] value;
//已经使用的字符数组的数量
int count;

1.3 StringBuilder为什么会抛出ArrayIndexOutOfBoundsException异常?

        我们看回AbstractStringBuilder的append()方法源码的第五行,ensureCapacityInternal()方法是检查StringBuilder对象的原char数组的容量能不能盛下新的字符串,如果盛不下就调用expandCapacity()方法对char数组进行扩容。

    private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0) {
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity));
        }
    }
  • 扩容的逻辑就是new一个新的char数组,新的char数组的容量是原来char数组的两倍再加2。在把扩容后的数组返回去,再通过Arrays.copyOf()函数将原数组的内容复制到新数组,最后将指针指向新的char数组。
    private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int newCapacity = (value.length << 1) + 2;
        if (newCapacity - minCapacity < 0) {
            newCapacity = minCapacity;
        }
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
            ? hugeCapacity(minCapacity)
            : newCapacity;
    }

Arrys.copyOf()方法

    public static char[] copyOf(char[] original, int newLength) {
        char[] copy = new char[newLength];
        //System拷贝数组
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

AbstractStringBuilder的append()方法源码的第六行,是将String对象里面char数组里面的内容拷贝到StringBuilder对象的char数组里面,代码如下:

str.getChars(0, len, value, count);

getChars()方法

    public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
        if (srcBegin < 0) {
            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);
    }

拷贝流程见下图:
在这里插入图片描述
假设现在有两个线程同时执行了StringBuilder的append()方法,两个线程都执行完了第五行的ensureCapacityInternal()方法,此刻count=5。
在这里插入图片描述
这个时候线程1的cpu时间片用完了,线程2继续执行。线程2执行完整个append()方法后count变成6了。
在这里插入图片描述
线程1继续执行第六行的str.getChars()方法的时候拿到的count值就是6了,执行char数组拷贝的时候就会抛出ArrayIndexOutOfBoundsException异常(Array Index OutOf BoundsException)。

1.4 String的安全性

  • 同样String的操作中虽然没有添加锁的,但是final修饰的String,代表了String的不可继承性,final修饰的char[]代表了被存储的数据不可更改性。

一、Java String类为什么是final的?

1.为了实现字符串池

2.为了线程安全

3.为了实现String可以创建HashCode不可变性

二、Java final的用途?

1、final可以修饰类,方法和变量,

2、final修饰的类,不能被继承,即它不能拥有自己的子类,

3、final修饰的方法,不能被重写,
4、final修饰的变量,无论是类属性、对象属性、形参还是局部变量,都需要进行初始化操作。 

注: final代表了不可变,但仅仅是引用地址不可变,并不代表了数组本身不会变
在这里插入图片描述
        final也可以将数组本身改变的,这个时候,起作用的还有private,正是因为两者保证了String的不可变性。

那么为什么保证String不可变呢?

因为只有当字符串是不可变的,字符串池才有可能实现。

        字符串池的实现:可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。

        因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。

        因为字符串是不可变的,所以在它创建的时候HashCode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。

1.5 SpringBuffer与StringBuilder

因为SpringBufferStringBuilder都是继承AbstractStringBuilder类,并且去除了个别方法,大部分都是通过super.xxx来调用AbstractStringBuilder的方法来实现的。

还有就是因为SpringBuffer更多考虑的是并发下的大数据量的操作,所以其对字符串进行修改的方法都用synchronized进行修饰。

还有就是SpringBuffer中的方法中比StringBuilder多了toStringCache的变量,其中toStringCache 是字符数组 value 复制的一个副本,每当 value 发生改变时,toStringCache 都会被置为空。

这就保证了每次只要 StringBuffer 对象发生改变,再调用 toString() 方法就必然产生一个新的 toStringCache 数组,从而保证了引用了旧的 toStringCache 的字符串对象不会发生改变。

即使多个线程同时访问 StringBuffer 对象,某一时刻也只有一个线程能够进入修改 toStringCachevalue 的代码块,这通过修饰 StringBuffer 方法的 synchroinzed 关键字来保证。

    @Override
    public synchronized String toString() {
        if (toStringCache == null) {
            toStringCache = Arrays.copyOfRange(value, 0, count);
        }
        return new String(toStringCache, true);
    }

那么 StringBuffertoStringCache 存在的必要性如何?它调用的是下面这个构造方法来创建 String 对象,构造 String 对象时直接共享传入的字符数组 value,而不是像 public String(char value[]) 一样复制一份。

     /*
    * Package private constructor which shares value array for speed.
    * this constructor is always expected to be called with share==true.
    * a separate constructor is needed because we already have a public
    * String(char[]) constructor that makes a copy of the given char[].
    */
    String(char[] value, boolean share) {
        // assert share : "unshared not supported";
        this.value = value;
    }

从源码中可以知道,StringBuffer 中使用 toStringCache 通过共享一个字符数组,提供构造 String 的速度,这是一个好处。另一个好处是连续多次调用 toString() 方法是不会产生多个内容相同的 String 对象。

但是,这些好处仅仅是在多次调用 toString() 方法且 StringBuffer 对象没有发生改变时才能体现。而实际编写代码的过程中,很少会在没有修改 StringBuffer 的情况下重复调用 toString() 方法,所以它并没有太大的实际作用。

2. 总结一下:

  • String 类型和 StringBuffer 的主要性能区别:String 是不可变的对象, 因此在每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象,所以经常改变内容的字符串最好不要用 String ,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后, JVM 的 GC 就会开始工作,性能就会降低。

  • StringBuffer相对于StringBuilder效率要相对低一点,但也远比String要高的多。效率低的原因:对于StringBuffer来说更多的考虑到了多线程的情况,在进行字符串操作的时候,它使用了synchronized关键字,对方法进行了同步处理。因此StringBuffer适用于多线程环境下的大量操作。

  • 在进行多线程处理的时候,如果多个线程对于这一个对象同时产生操作,会产生预期之外的结果。对于StringBuilder来说,执行效率虽然高,但是因为线程不安全,所以不建议在多线程的环境下对同一个StringBuilder对象进行操作。因此StringBuilder适用于单线程环境下的大量字符串操作。

  • StringBuilder和StringBuffer的初始化容量都是16,扩展的话,每次是2倍加2.

    public StringBuilder() {
        super(16);
    }

    public StringBuffer() {
        super(16);
    }
    
    int newCapacity = (value.length << 1) + 2;
StringStringBufferStringBuilder
执行速度最差其次最高
线程安全线程安全线程安全线程不安全
使用场景少量字符串操作多线程环境下的大量操作单线程环境下的大量操作

文章参考:https://blog.csdn.net/kingzone_2008/article/details/9220691
https://www.runoob.com/w3cnote/java-different-of-string-stringbuffer-stringbuilder.html
https://blog.csdn.net/Turniper/article/details/111112824
为什么 StringBuffer 有 toStringCache 而 StringBuilder 没有?

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值