通过JDK8源码深入学习String,StringBuffer,StringBuilder

时间:2022-08-12 04:01:54

String,StringBuffer,StringBuilder的异同:

1.String为定长的,StringBuffer,StringBuilder为可变长,缓冲区满了会成倍扩容。

2.StringBuffer为线程安全的,StringBuilder为非线性安全。

由stringbuffer的源码可见方法都加上了synchronized锁,势必对性能有所影响。

通过JDK8源码深入学习String,StringBuffer,StringBuilder

3.StringBuffer和StringBuilder均继承了AbstractStringBuilder类,StringBuffer和StringBuilder初始化字符串缓存区方法相同。

     只举stringbuffer的例子,stringbuilder类似,可见源码。

     StringBuffer str1 = new StringBuffer();

     此时初始化构造一个没有字符串的缓存区,初始容量默认为16字符。

     StringBuffer str2 = new StringBuffer(str);

     此时初始化构造一个内容为str的字符串缓冲区。字符串缓冲区的初始容量是(16+str.length)字符。

通过JDK8源码深入学习String,StringBuffer,StringBuilder

通过JDK8源码深入学习String,StringBuffer,StringBuilder

4.StringBuffer和StringBuilder的toString()方法均需要占用新的内存,内存大小为字符串的大小。

通过JDK8源码深入学习String,StringBuffer,StringBuilder

通过JDK8源码深入学习String,StringBuffer,StringBuilder

5.stringbuffer和stringbuilder占用内存

以stringbuilder为例:

StringBuilder str = new StringBuilder();

for (int i=0; i<129; i++){

    str.append( "a" );

}

从默认16字符开始,append()过程中,4次扩容复制 16 + 32 + 64 + 128 + 256 = 496 

还不算完,StringBuilder.toString() == return new String(value, 0, count)

总共需要 496 + 129 = 525字符,并若干次 内存复制。

不仅占用内存还影响GC性能。


优化建议(参考自http://calvin1978.blogcn.com/articles/stringbuilder.html):

1.初始化长度非常重要!!!

StringBuffer,StringBuilder的内部都有一个缓存区char[],不断的append()就是不断的往char[]里填东西的过程。

new StringBuilder() 时char[]的默认长度是16,然后,如果要append第17个字符,怎么办?

用System.arraycopy成倍复制扩容!!!!

这样一来有数组拷贝的成本,二来原来的char[]也白白浪费了要被GC掉。可以想见,一个129字符长度的字符串,经过了16,32,64, 128四次的复制和丢弃,合共申请了496字符的数组,在高性能场景下,这几乎不能忍。

所以,合理设置一个初始值多重要

但如果我实在估算不好呢?多估一点点好了,只要字符串最后大于16,就算浪费一点点,也比成倍的扩容好。

2.以Liferary的StringBuilder类为例

Liferay的StringBundler类提供了另一个长度设置的思路,它在append()的时候,不急着往char[]里塞东西,而是先拿一个String[]把它们都存起来,到了最后才把所有String的length加起来,构造一个合理长度的StringBuilder。

3.但还是浪费类一倍的char[]

浪费发生在最后一步,StringBuilder.toString()

//创建拷贝, 不共享数组
return new String(value, 0, count);

String的构造函数会用 System.arraycopy()复制一把传入的char[]来保证安全性不可变性,如果故事就这样结束,StringBuilder里的char[]还是被白白牺牲了。

为了不浪费这些char[],一种方法是用Unsafe之类的各种黑科技,绕过构造函数直接给String的char[]属性赋值,但很少人这样做。

另一个靠谱一些的办法就是重用StringBuilder。而重用,还解决了前面的长度设置问题,因为即使一开始估算不准,多扩容几次之后也够了。

4.重用StringBuilder

这个做法来源于JDK里的BigDecimal类(没事看看JDK代码多重要),后来发现Netty也同样使用。SpringSide里将代码提取成StringBuilderHolder,里面只有一个函数

public StringBuilder getStringBuilder() {
sb.setLength(0);
return sb;
}

StringBuilder.setLength()函数只重置它的count指针,而char[]则会继续重用,而toString()时会把当前的count指针也作为参数传给String的构造函数,所以不用担心把超过新内容大小的旧内容也传进去了。可见,StringBuilder是完全可以被重用的。

为了避免并发冲突,这个Holder一般设为ThreadLocal,标准写法见BigDecimal或StringBuilderHolder的注释

不过,如果String的长度不大,那从ThreadLocal里取一次值的代价还更大的多,所以也不能把这个ThreadLocalStringBuilder搞出来后,见到StringBuilder就替换。。。