在 Java 编程中,字符串操作是非常常见的需求。Java 提供了两种主要的可变字符串类:StringBuilder 和 StringBuffer。虽然它们的 API 几乎完全相同,但在多线程环境下,它们的行为却有很大的不同。本文将深入探讨这两个类的线程安全性,并分析它们在不同场景下的适用情况。
一、StringBuilder 和 StringBuffer 的基本介绍
1. StringBuilder
StringBuilder 是 Java 5 中引入的,它是一个可变的字符序列。它提供了与 StringBuffer 兼容的 API,但不保证线程安全。在单线程环境下,StringBuilder 的性能要优于 StringBuffer。
2. StringBuffer
StringBuffer 是 Java 早期版本就存在的,它同样表示一个可变的字符序列。与 StringBuilder 不同的是,StringBuffer 是线程安全的,它的所有公共方法都被 synchronized 修饰,因此可以在多线程环境下安全使用。
二、线程安全的本质区别
1. StringBuffer 的线程安全实现
StringBuffer 的线程安全性是通过在方法级别使用 synchronized 关键字实现的。例如,它的 append () 方法的实现如下:
@Override
public synchronized StringBuffer append(String str) {toStringCache = null;super.append(str);return this;
}
可以看到,append () 方法被 synchronized 修饰,这意味着在同一时刻,只有一个线程可以执行该方法。这种同步机制确保了多线程环境下的操作安全,但也带来了一定的性能开销。
2. StringBuilder 的非线程安全特性
StringBuilder 的方法没有使用 synchronized 修饰,因此它不是线程安全的。例如,它的 append () 方法实现如下:
@Override
public StringBuilder append(String str) {super.append(str);return this;
}
在多线程环境下,如果多个线程同时访问同一个 StringBuilder 实例并进行修改操作,可能会导致数据不一致或其他不可预期的结果。
三、线程安全的实际影响
1. 多线程环境下的问题示例
下面的代码演示了在多线程环境下使用 StringBuilder 可能出现的问题:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class StringSafetyDemo {private static StringBuilder builder = new StringBuilder();public static void main(String[] args) throws InterruptedException {ExecutorService executor = Executors.newFixedThreadPool(10);for (int i = 0; i < 1000; i++) {executor.submit(() -> {builder.append("a");});}executor.shutdown();while (!executor.isTerminated()) {Thread.sleep(100);}System.out.println("Builder length: " + builder.length());}
}
在这个示例中,我们创建了一个包含 10 个线程的线程池,每个线程向同一个 StringBuilder 实例追加字符 “a”。理论上,最终 StringBuilder 的长度应该是 1000,但实际上运行这段代码可能会得到不同的结果(一般得到的长度都小于1000),甚至可能抛出 StringIndexOutOfBoundsException 异常。
这里我们详细演示一下为什么会出现这种状况
StringBuilder
的append()
方法大致实现如下:
public StringBuilder append(String str) {// 1. 检查当前容量是否足够if (count + str.length() > value.length) {expandCapacity(count + str.length()); // 扩容操作}// 2. 将字符串复制到内部字符数组str.getChars(0, str.length(), value, count);// 3. 更新长度计数器count += str.length();return this;
}
假设有两个线程(T1 和 T2)同时调用append("a")
,初始状态:
value
数组长度为 10,已使用长度count = 8
(即value[8]
和value[9]
为空)
场景 1:两个线程同时检查容量
- T1 执行步骤 1:计算新长度
8+1=9 ≤ 10
,认为不需要扩容。 - T2 执行步骤 1:此时
count
仍为 8(T1 尚未更新),同样认为不需要扩容。 - T1 执行步骤 2:将
'a'
写入value[8]
。 - T2 执行步骤 2:将
'a'
写入value[8]
(覆盖 T1 写入的数据)。 - T1 执行步骤 3:
count
变为 9。 - T2 执行步骤 3:
count
变为 10(本应是 11,但 T2 的写入被覆盖)。
结果:两次append
操作只增加了一个字符,最终长度为 10 而非 11。
场景 2:一个线程扩容时另一个线程写入
- T1 执行步骤 1:发现需要扩容,调用
expandCapacity(9)
创建新数组(长度 20)。 - T2 执行步骤 1:由于 T1 尚未更新
value
引用,T2 仍使用旧数组,写入value[8]
。 - T1 继续执行:将旧数组内容复制到新数组,但 T2 写入的
value[8]
未被复制。 - T1 更新
value
引用:T2 的写入丢失,且count
可能被错误更新。
结果:数据丢失,最终长度可能小于预期。
2. 使用 StringBuffer 解决线程安全问题
如果将上面的代码中的 StringBuilder 替换为 StringBuffer,就可以确保结果的正确性:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class StringBufferThreadSafetyDemo {private static StringBuffer buffer = new StringBuffer();public static void main(String[] args) throws InterruptedException {ExecutorService executor = Executors.newFixedThreadPool(10);for (int i = 0; i < 1000; i++) {executor.submit(() -> {buffer.append("a");});}executor.shutdown();while (!executor.isTerminated()) {Thread.sleep(100);}System.out.println("Buffer length: " + buffer.length());}
}
无论运行多少次,这段代码都会输出 “Buffer length: 1000”,因为 StringBuffer 的方法是线程安全的。
四、性能比较
由于 StringBuffer 的方法是同步的,而 StringBuilder 的方法不是,因此在单线程环境下,StringBuilder 的性能要明显优于 StringBuffer。下面是一个简单的性能测试示例:
public class ATest {private static final int ITERATIONS = 100000;public static void main(String[] args) {// 测试StringBuilder性能long startTime = System.currentTimeMillis();StringBuilder sb = new StringBuilder();for (int i = 0; i < ITERATIONS; i++) {sb.append("test");}long endTime = System.currentTimeMillis();System.out.println("StringBuilder time: " + (endTime - startTime) + " ms");// 测试StringBuffer性能startTime = System.currentTimeMillis();StringBuffer sbf = new StringBuffer();for (int i = 0; i < ITERATIONS; i++) {sbf.append("test");}endTime = System.currentTimeMillis();System.out.println("StringBuffer time: " + (endTime - startTime) + " ms");}
}
在我的idea上运行这段代码,StringBuilder 的执行时间大约是 5 毫秒,而 StringBuffer 的执行时间大约是 15 毫秒。可以看到,StringBuilder 的性能优势非常明显。
五、适用场景
1. 使用 StringBuilder 的场景
- 单线程环境下的字符串拼接操作
- 需要高性能的字符串处理场景
2. 使用 StringBuffer 的场景
- 多线程环境下的字符串拼接操作
- 需要确保线程安全的字符串处理场景
六、总结
StringBuilder 和 StringBuffer 都是 Java 中用于处理可变字符串的类,但它们的线程安全性不同。StringBuffer 通过同步方法保证了线程安全,适用于多线程环境;而 StringBuilder 没有同步开销,性能更高,适用于单线程环境