结论
如果你在写字符串拼接的代码时,只用过 String,或者用过 StringBuilder, StringBuffer 不是很明白其优缺点。那么本文就是为你准备的。
先说结论:
- String: 操作方便,但高频次拼接字符串时性能低;
- StringBuilder: 高频次拼接字符串性能高出 String 类不少,但是并发情况下容易出现现成不安全;
- StringBuffer: 性能同样优异,略微比 StringBuilder 次一些,但是解决了 StringBuilder 并发下的线程不安全情况。
至此,你可以选择记住结论离开了。但我更希望你继续看下去,接下来会基于实验,演示 String 到底比 StringBuilder 慢多少,并且实践一下,写个精简(10行代码左右)的 StringBuilder 和 StringBuffer,明白其内在原理。
实验
首先准备一个辅助的时间统计类 Timer,计算一段代码的耗时。
// Timer.java
public class Timer {
private long startTime = System.currentTimeMillis();
public long finish() {
return System.currentTimeMillis() - startTime;
}
}
使用示例
Timer timer = new Timer();
// 需要计算耗时的代码
System.out.println("耗时" + timer.finish() + "毫秒");
String vs String Builder
这里主要来比较 String 与 StringBuilder 的性能差。测试内容:将10万个单词拼接词一个句子。
// StringTest.java
public class StringTest {
private final int times = 100000;
public static void main(String[] args) throws Exception {
StringTest t = new StringTest();
t.test1();
t.test2();
}
/**
* 10000次 String + String 方式的单词拼接耗时测试
*/
private void test1() {
String sentence = "";
Timer timer1 = new Timer();
for (long i = 0; i < times; i++) {
String word = String.valueOf(i); // 这里使用阿拉伯数字代替单词
sentence += word;
}
// System.out.println(sentence); // 打印出拼接结果
System.out.println("String + String 方式拼接" + times + "次单词,耗时 " + timer1.finish() + " 毫秒");
}
/**
* 10000次 StringBuilder::append() 方式的单词拼接耗时测试
*/
private void test2() {
StringBuilder sentence = new StringBuilder();
Timer timer2 = new Timer();
for (long i = 0; i < times; i++) {
String word = String.valueOf(i); // 这里使用阿拉伯数字代替单词
sentence.append(word);
}
// System.out.println(sentence.toString()); // 打印出拼接结果
System.out.println("StringBuilder::append() 方式拼接" + times + "次单词,耗时 " +timer2.finish() + " 毫秒");
}
}
实验输出(MacBook Air 2C8G)
String + String 方式拼接100000次单词,耗时 37150 毫秒
StringBuilder::append() 方式拼接100000次单词,耗时 13 毫秒
实验结论:拼接10万次字符串的耗时,String类要比StringBuilder多2800倍左右(仅本次实验)。
StringBuilder vs String Buffer
接下来我们比较并发情况下的 StringBuilder 和 StringBuffer。
实验方法如下,初始化一个StringBuilder 或 StringBuffer 对象,开两个线程,每个线程分别调用100000次 append() 方法,往里面塞长度为1的100000字符串。按照预期,该对象持有的字符串,长度应该为200000。
public class StringTest {
private final int times = 100000;
public static void main(String[] args) throws Exception {
StringTest t = new StringTest();
t.test3();
t.test4();
}
/**
* 并发环境测试 StringBuilder
*/
private void test3() {
StringBuilder sb = new StringBuilder();
try {
Thread t1 = new Thread(() -> { for (int i=0; i<times; i++) sb.append("a"); });
Thread t2 = new Thread(() -> { for (int i=0; i<times; i++) sb.append("b"); });
t1.start(); t2.start();
t1.join(); t2.join();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("StringBuilder::append() " + times * 2 + "次后的长度: " + sb.length()); // 多线程情况下已经有问题了
}
/**
* 并发环境测试 StringBuffer
*/
private void test4() {
StringBuffer sb = new StringBuffer();
try {
Thread t1 = new Thread(() -> { for (int i=0; i<times; i++) sb.append("a"); });
Thread t2 = new Thread(() -> { for (int i=0; i<times; i++) sb.append("b"); });
t1.start(); t2.start();
t1.join(); t2.join();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("StringBuffer::append() " + times * 2 + "次后的长度: " + sb.length());
}
}
实验输出
StringBuilder::append() 200000次后的长度: 100075
StringBuffer::append() 200000次后的长度: 200000
结论: StringBuilder 的结果不符合预期,也就是我们说的并发情况下的线程不安全。
分析
String
java.lang.String.java 部分源码
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
...
}
留意 String 类里的 value[] 属性,“The value is used for character storage.” 表示,此属性主要用来存储字符串的字符数组。
且是 final,不可被修改的。每次执行
sentence += word;
jvm 都会优化为
sentence = new StringBuilder(sentence).append(word).toString();
每次都会产生一个新的字符串赋值给 sentence, 且由于这个sentence越来越长,每次新产生字符串都要发生字节拷贝,非常耗时。
StringBuilder
StringBuilder 和 StringBuffer 类似,为了避免每次都要产生新的字符串,都预先使用一个字符数组,每次拼接字符串,都是把新串合并到预先准备好的字符数组,避免了大量的字符拷贝。
具体实现,就是 StringBuilder 和 StringBuffer 都实现了 java.lang.Appendable 接口
// java.lang.AppendAble
public interface Appendable {
Appendable append(CharSequence csq) throws IOException;
Appendable append(CharSequence csq, int start, int end) throws IOException;
Appendable append(char c) throws IOException;
}
String 对象则实现了 CharSequence 接口,所以 StringBuilder 的 append() 方法可以接受一个 String 类型的传参。接下来再具体看下 append() 的具体实现
// java.lang.AbstractStringBuilder
abstract class AbstractStringBuilder implements Appendable, CharSequence {
// 存储当前字符串的所有字符
char[] value;
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
// 扩容 value 字符数组的长度,每次扩容会发生字符数组拷贝。
// 为了减少扩容次数,扩容算法大致思路为,不断翻倍 16->32->64->128->256... 注:这不是精准的扩容算法,具体请看源码
ensureCapacityInternal(count + len);
// 拷贝 str 的字符数组,至 value 字符数组的结尾
str.getChars(0, len, value, count);
count += len;
return this;
}
}
由此可以看出,StringBuilder 和 StringBuffer 均持有一个字符数组,来让新的子串加入进来。通过翻倍扩容的方式,避免大字符数组频繁发生拷贝。
StringBuffer
StringBuffer 和 StringBuilder 均继承自 AbstractStringBuilder, 使用方法几乎一致。以下主要分析为啥 StringBuilder 是线程不安全,而 StringBuffer 是线程安全的。
我们分别看下这两个类的 append 方法源码
// java.lang.StringBuilder
public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, CharSequence {
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
}
// java.lang.StringBuffer
public final class StringBuffer extends AbstractStringBuilder implements java.io.Serializable, CharSequence {
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
}
可以看出,它们的相同之处。都是以委托方式,具体逻辑交由父类的 append() 方法处理,而这个方法,我们再上文有讲解。
重点说下不同之处,StringBuffer 的 append() 方法多了用 synchronized 关键词去描述。表示这是一个同步方法,只允许单线程去访问,其他线程需要去等待。这样也就保证了 append 方法的线程安全。
DIY
有了以上的知识储备,我们可以练练手,实现一个自己的 StringBuilder 和 StringBuffer,这里直接贴代码了。
MyStringBuilder
// MyStringBuilder.java
class MyStringBuilder {
private char[] chars;
private int count;
public MyStringBuilder() {
chars = new char[100000*10];
count = 0;
}
public void append(String substr) {
substr.getChars(0, substr.length(), chars, count);
count = substr.length() + count;
}
public int length() {
return count;
}
@Override
public String toString() {
return new String(chars, 0, count);
}
}
MyStringBuffer
class MyStringBuffer extends MyStringBuilder {
@Override
public synchronized void append(String substr) {
super.append(substr);
}
}
本文完。
附本文全部代码
package string;
public class StringTest {
private final int times = 100000;
public static void main(String[] args) throws Exception {
StringTest t = new StringTest();
t.test1();
t.test2();
t.test3();
t.test4();
t.test5();
t.test6();
t.test7();
t.test8();
}
/**
* 10000次 String + String 方式的单词拼接耗时测试
*/
private void test1() {
String sentence = new String("");
Timer timer1 = new Timer();
for (long i = 0; i < times; i++) {
String word = String.valueOf(i); // 这里使用阿拉伯数字代替单词
sentence += word;
}
// System.out.println(sentence); // 打印出拼接结果
System.out.println("String + String 方式拼接" + times + "次单词,耗时 " + timer1.finish() + " 毫秒");
}
/**
* 10000次 StringBuilder::append() 方式的单词拼接耗时测试
*/
private void test2() {
StringBuilder sentence = new StringBuilder();
Timer timer2 = new Timer();
for (long i = 0; i < times; i++) {
String word = String.valueOf(i); // 这里使用阿拉伯数字代替单词
sentence.append(word);
}
// System.out.println(sentence.toString()); // 打印出拼接结果
System.out.println("StringBuilder::append() 方式拼接" + times + "次单词,耗时 " +timer2.finish() + " 毫秒");
}
/**
* 并发环境测试 StringBuilder
*/
private void test3() {
StringBuilder sb = new StringBuilder();
try {
Thread t1 = new Thread(() -> { for (int i=0; i<times; i++) sb.append("a"); });
Thread t2 = new Thread(() -> { for (int i=0; i<times; i++) sb.append("b"); });
t1.start(); t2.start();
t1.join(); t2.join();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("StringBuilder::append() " + times * 2 + "次后的长度: " + sb.length()); // 多线程情况下已经有问题了
}
/**
* 并发环境测试 StringBuffer
*/
private void test4() {
StringBuffer sb = new StringBuffer();
try {
Thread t1 = new Thread(() -> { for (int i=0; i<times; i++) sb.append("a"); });
Thread t2 = new Thread(() -> { for (int i=0; i<times; i++) sb.append("b"); });
t1.start(); t2.start();
t1.join(); t2.join();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("StringBuffer::append() " + times * 2 + "次后的长度: " + sb.length());
}
/**
* 使用自己写的 MyStringBuilder 去测试字符串拼接
*/
private void test5() {
MyStringBuilder s = new MyStringBuilder();
Timer timer2 = new Timer();
for (long i = 0; i < times; i++) {
s.append(String.valueOf(i));
}
// System.out.println(s.toString());
System.out.println("MyStringBuilder::append() 方式拼接" + times + "次单词,耗时 " +timer2.finish() + " 毫秒");
}
/**
* 使用自己写的 MyStringBuffer 去测试字符串拼接
*/
private void test6() {
MyStringBuffer s = new MyStringBuffer();
Timer timer2 = new Timer();
for (long i = 0; i < times; i++) {
s.append(String.valueOf(i));
}
// System.out.println(s.toString());
System.out.println("MyStringBuffer::append() 方式拼接" + times + "次单词,耗时 " +timer2.finish() + " 毫秒");
}
/**
* 使用自己写的 MyStringBuilder 去演示并发情况下的线程不安全
*/
private void test7() {
MyStringBuilder sb = new MyStringBuilder();
try {
Thread t1 = new Thread(() -> {
for (int i=0; i<times; i++) {
sb.append("a");
}
});
Thread t2 = new Thread(() -> {
for (int i=0; i<times; i++) {
sb.append("b");
}
});
t1.start();
t2.start();
t1.join();
t2.join();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(sb.length()); // 多线程情况下已经有问题了
}
/**
* 使用自己写的 MyStringBuffer 去演示并发情况下的线程安全
*/
private void test8() throws Exception {
int times = 100000;
MyStringBuilder sb = new MyStringBuilder();
Thread t1 = new Thread(() -> {
for (int i=0; i<times; i++) {
sb.append("a");
}
});
Thread t2 = new Thread(() -> {
for (int i=0; i<times; i++) {
sb.append("b");
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(sb.length());
}
}
class MyStringBuilder {
private char[] chars;
private int count;
public MyStringBuilder() {
chars = new char[100000*10];
count = 0;
}
public void append(String substr) {
substr.getChars(0, substr.length(), chars, count);
count = substr.length() + count;
}
public int length() {
return count;
}
@Override
public String toString() {
return new String(chars, 0, count);
}
}
class MyStringBuffer extends MyStringBuilder {
@Override
public synchronized void append(String substr) {
super.append(substr);
}
}
class Timer {
private long startTime = System.currentTimeMillis();
public long finish() {
return System.currentTimeMillis() - startTime;
}
}