结论
String:不需要修改字符串,或者修改次数极少 。(String的+=其实不保证线程安全,String的+=操作底层是StringBuilder实现的,线程不安全)
StringBuffer:需要频繁修改字符串,多线程场景,需要保证线程的安全。
StringBuilder:需要频繁修改字符串,单线程场景,需要修改速度快。
原因
为什么String的访问速度快,为什么不能修改
String底层是一个常量,访问一个常量很快的,常量不支持修改。
java
private final char value[]
既然String不能被修改,那+=是怎么回事
java
public class Main {
public static void main(String[] args) {
String s = "123";
s += "456";
System.out.println(s);
}
}
输出
out
123456
这样看起来,好像是在s后面添加了字符串"456",但是实际上,是改变了s的引用。
下面这段代码可以验证,s已经不是之前的s了。
java
public class Main {
public static void main(String[] args) {
String s = "123";
String b = s;
System.out.println(b == s);
s += "456";
System.out.println(b);
System.out.println(b == s);
}
}
输出
out
true
123
false
所以String的修改不是修改原字符串,而是替换字符创指向的引用。
真正的修改原来字符串会有下面的现象
java
public class Main {
public static void main(String[] args) {
StringBuilder s = new StringBuilder("123");
StringBuilder b = s;
System.out.println(b == s);
s.append("456");
System.out.println(b);
System.out.println(b == s);
}
}
输出
out
true
123456
true
验证StringBuilder线程不安全,StringBuffer线程安全
通过对比多线程场景下 StringBuilder 和 StringBuffer 的拼接结果,验证二者的线程安全性:
一个线程负责在末尾添加"1",另一个线程负责在末尾添加"2"。
假如线程安全,那么要么全是1后面接全是2,要么全是2后面接全是1。
如果结果不是这两个,那么线程不安全。
java
public class Main {
public static void main(String[] args) throws InterruptedException {
int repeatCount = 100000;
String result1 = "1".repeat(repeatCount) + "2".repeat(repeatCount);
String result2 = "2".repeat(repeatCount) + "1".repeat(repeatCount);
StringBuilder stringBuilder = new StringBuilder();
Thread thread1 = new Thread(() -> {
stringBuilder.append("1".repeat(repeatCount));
});
Thread thread2 = new Thread(() -> {
stringBuilder.append("2".repeat(repeatCount));
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(stringBuilder.toString().equals(result1) || stringBuilder.toString().equals(result2));
StringBuffer stringBuffer = new StringBuffer();
thread1 = new Thread(() -> {
stringBuffer.append("1".repeat(repeatCount));
});
thread2 = new Thread(() -> {
stringBuffer.append("2".repeat(repeatCount));
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(stringBuffer.toString().equals(result1) || stringBuffer.toString().equals(result2));
}
}
输出
out
false
true
如果抛出ArrayIndexOutOfBoundsException,也能证明,ArrayIndexOutOfBoundsException 是 StringBuilder 线程不安全的典型严重表现 ,核心原因是多线程同时操作 StringBuilder 底层的字符数组时,导致数组扩容逻辑混乱,最终触发数组下标越界。
SrringBuffer线程安全的原因,StringBuilder速度快的原因
SrringBuffer对方法加了同步锁,保证了线程安全,但是限制了运行速度。
StringBuilder没有同步锁,所以线程不安全,由于没有锁的限制,执行速度却快。
面试
如果不修改或者修改次数极少,可以用String。
如果需要频繁修改,
多线程下,必须使用StringBuffer来保证线程安全。
单线程下,可以使用StringBuilder比StringBuffer快一些。