1 缘起
比较有意思的是,学习锁消除的过程中,有人讲到StringBuffer在方法内构建,不会被其他方法引用时,StringBuffer的锁会被消除,
于是,顺便看了一下同源的StringBuidler为什么线程不安全,以及为什么多线程不安全,和带来的问题,
有了这篇文章,分享出来,帮助读者轻松应对知识交流与考核。
2 StringBuilder
StringBuilder用于缓存字符串的容器,是StringBuffer的高性能版本,因为,StringBuilder适用于单线程,多线程下无法保证程序正常执行。建议优先使用StringBuilder,多数场景下,效率更高。
StringBuilder继承AbstractStringBuilder,而StringBuffer也是继承该类,所以,StringBuilder和StringBuffer是兼容的。
位置:java.lang.StringBuilder
2.1 StringBuilder线程不安全是指什么?
StringBuilder的多线程不安全是指程序不能正常执行,即数组越界异常,而不是数据错乱问题。
2.1.1 测试样例
package com.monkey.java_study.lock;
/**
* 锁消除测试.
*
* @author xindaqi
* @since 2023-06-23 16:04
*/
public class LockEliminateTest {
static StringBuilder sb2 = new StringBuilder();
public static void main(String[] args) throws Exception {
LockEliminateTest lockEliminateTest = new LockEliminateTest();
for (int i = 0; i < 100; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
sb2.append("test");
}
}).start();
}
Thread.sleep(100);
System.out.format(">>>>>>StringBuilder:length:%d\n", sb2.length());
}
}
2.1.2 测试结果
多线程情况下,StringBuilder出现数组越界,无法正常添加数据,程序异常。
异常信息如下图所示:
2.2 为什么线程不安全?
结论:因为AbstractStringBuilder中的append方法中使用了全局变量count,多线程无法保证数组正常扩充,因此,出现数组越界的异常。
先从append方法说起,StringBuilder继承AbstractStringBuilder,而StringBuilder中直接使用了父类的append方法。
既然StringBuilder直接使用了AbstractStringBuilder的append方法,我们直接探究这个父类的方法,源码如下,使用的全局变量count作为数组扩容的参数:ensureCapacityInternal。
数组扩容系列操作源码如下,如果传入的值大于存储数据的字符数组长度,则拷贝数据到新的数组中,保证数据可以正常存储。
问题就出现在这里,多线程出现数组没有及时扩充,使用str.getChars时,导致数组越界。
现在推演一下:
缓存数据的数组arr长度为10,已占用6个空位,还剩4个空位
即初始count为6,
两个线程T1和T2,均携带4个字符,向数组写入数据,
此时:minimumCapacity=count+len=6+4=10
线程T1:
miniumCapacity-value.length=0不满足扩容条件,
此时数据写入arr,刚好填满,还未执行到count+=len时,
线程T2:
开始执行ensureCapacityInternal,此时,count+len=6+4=10,依旧不会触发扩容,
线程T1:
执行count+=len,即count=10,
当T2执行到str.getChars时,count为10,出现数组不够用,导致数组越界。
即:T2执行str.getChars(0, 4, [a1, …, a10], 10)
srcBegin=0
srcEnd=4
dst=[a1, …, a10]
dstBegin=10
在826行出现异常,即Sysgem.arrayCopy
这个时候dst长度为10,而新写入的数据从10开始,因此,无法再写入数据了,抛出异常。
多线程出现的时间差,导致数组没有正常被扩容,
最后无法写入数据,数组越界后抛出异常。
测试样例:
char[] dst = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'};
String str = "test";
str.getChars(0, 4, dst, 10);
System.out.println(">>>>>Char array:" + Arrays.toString(dst));
同款异常如下:
2.3 StringBuilder多线程不安全带来的问题?
程序无法正常执行(抛出数组越界的异常),而不是数据错乱问题。
3 小结
(1)StringBuilder的多线程不安全是指程序不能正常执行,即数组越界异常,而不是数据错乱问题。
(2)AbstractStringBuilder中的append方法中使用了全局变量count,多线程无法保证数组正常扩充,因此,出现数组越界的异常。
(3)StringBuilde多线程不安全带来的问题是程序无法正常运行。