首先明确几点结论:
- 可变性:就String和后两者相比,String是字符串常量,后两者是字符串变量。
- 线程安全性:就后两者相比,StringBuilder不是线程安全的,而StringBuffer是线程安全的。
- 性能:就效率来说,通常情况下:StringBuilder>StringBuffer>String。
分析
一、可变性
虽然都是通过一个char数组来存储数据,但是String的char数组是final修饰的,因此是不可变的。而后两者类都是继承自AbstractStringBuilder,它的char数组没有声明为final类型,是可变的。
二、线程安全性
在这一节,我们可以通过一个对比实验来说明为什么说StringBuilder不是线程安全的,而StringBuffer是线程安全的
2.1 StringBuilder不是线程安全的
首先给出代码
StringBuilder stringBuilder = new StringBuilder();
for(int i =0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
for(int j = 0;j < 1000; j++) {
stringBuilder.append("a");
}
}
}).start();
}
Thread.sleep(1000);
System.out.println(stringBuilder.length());
在这段代码中,我们通过创建10个线程,每个线程通过StringBuilder的append的操作向字符串追加1000个字符,最后统计字符串长度。理论上的长度应该是10*1000=10000才对。
但是我们看结果
抛出了一个ArrayIndexOutOfBoundsException异常,并且字符串长度为7824。而且跑多次会发现异常不是必现的。
1、我们来分析问什么长度不是10000。
我们来看StringBuilder的append方法:
其继承了父类的方法,那我们去看一下父类AbstractStringBuilder的append方法
在这个方法中我们可以看到有一个count+=len,len是追加的长度。假设两个线程同时执行到这一行,拿到的count都是10,要增加的值len都为1,那么两个线程分别对其加1,将数据赋值给count,最终两个线程执行完,得到的结果是11,而不是12。因此我们给出的实验结果不是10000。
2、为什么会抛出不必现的ArrayIndexOutOfBoundsException异常
同样我们继续分析上面append方法中的ensureCapacityInternal()方法,其表示判断char数组能否盛的下新的字符串,传入的参数表示即将生成的新字符串长度,value表示char数组。如果不能盛下,则调用expandCapacity()方法进行扩容。
而扩容则是通过new一个新的数组实现,其长度为原数组长度两倍再加2,然后通过System.arrayCopy()将原数组内容拷贝到新的数组,最后将value引用指向该数组。
Arrays.copyOf()实际上便是生成新数组的过程。
ensureCapacityInternal()方法做完了扩容,str.getChar()方法便是追加字符的操作。
假设value长度为6,而两个线程同时执行到ensureCapacityInternal()方法时count=5,因为增加的字符len=1,所以两个线程都不会进行扩容。假设线程一执行完str.getChar()方法,时间片用完,此时count变成了6,当线程二执行str.getChar()时则会出现这种越界的异常。
2.2 StringBuffer是线程安全的
得到的结果是10000。
当我们观察StringBuffer的append方法时,可以看到该方法使用了synchronized关键字,这就是StringBuffer线程安全的原因了。
三、性能
因为String的值不能改变,因此都在对其操作时,实际是让其引用指向一个新的字符串地址。因此相对来说,空间和时间上的开销都有所浪费。而相对于StringBuilder来说,StringBuffer增加了线程安全性,因此性能上也有所降低。