String,StringBuffer,StringBuilder的异同:
1.String为定长的,StringBuffer,StringBuilder为可变长,缓冲区满了会成倍扩容。
2.StringBuffer为线程安全的,StringBuilder为非线性安全。
由stringbuffer的源码可见方法都加上了synchronized锁,势必对性能有所影响。
3.StringBuffer和StringBuilder均继承了AbstractStringBuilder类,StringBuffer和StringBuilder初始化字符串缓存区方法相同。
只举stringbuffer的例子,stringbuilder类似,可见源码。
StringBuffer str1 = new StringBuffer();
此时初始化构造一个没有字符串的缓存区,初始容量默认为16字符。
StringBuffer str2 = new StringBuffer(str);
此时初始化构造一个内容为str的字符串缓冲区。字符串缓冲区的初始容量是(16+str.length)字符。
4.StringBuffer和StringBuilder的toString()方法均需要占用新的内存,内存大小为字符串的大小。
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就替换。。。