本文为joshua317原创文章,转载请注明:转载自joshua317博客 https://www.joshua317.com/article/241
一、简单了解下,String,StringBuilder和StringBuffer的区别在哪?
String 是 Java 语言非常基础和重要的类,提供了构造和管理字符串的各种基本逻辑。String是只读字符串,它并不是基本数据类型,而是一个对象。从底层源码来看是一个final类型的字符数组,所引用的字符串不能被改变,一经定义,无法再增删改。也由于它的不可变性,类似拼接、裁剪字符串等动作,每次对String的操作都会生成新的 String对象
private final char value[];
StringBuffer是为解决上面提到拼接产生太多中间对象的问题而提供的一个类,可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。StringBuffer 本质是一个线程安全的可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销,所以除非有线程安全的需要,不然还是推荐使用它的后继者,也就是 StringBuilder。
StringBuilder是 Java 1.5 中新增的,在能力上和 StringBuffer 没有本质区别,但是它去掉了线程安全的部分,减小了开销,是绝大部分情况下进行字符串拼接的首选。
StringBuffer和StringBuilder他们两都继承了AbstractStringBuilder抽象类,从AbstractStringBuilder 抽象类中我们可以看到
/**
* The value is used for character storage.
*/
char[] value;
StringBuilder和StringBuffer的内部实现跟String类一样,都是通过一个char数组存储字符串的,不同的是String类里面的char数组是final修饰的,是不可变的,而StringBuilder和StringBuffer的char数组是可变的。所以在进行频繁的字符串操作时,建议使用StringBuffer和 StringBuilder来进行操作。 另外StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
当我们需要对字符串进行大量修改时,推荐使用StringBuffer
和StringBuilder
类。
与String
类不同,StringBuffer
和StringBuilder
类的对象可以反复修改,而不会留下大量新的未使用对象。
StringBuilder
类是从Java 5开始引入的,StringBuffer
和StringBuilder
之间的主要区别在于StringBuilders
方法不是线程安全的(不同步)。
建议尽可能使用StringBuilder
类,因为它比StringBuffer
更快。 但是,如果需要线程安全性,最好是使用StringBuffer
类。
二、代码编程说明StringBuilder不是线程安全的
接下来,我们用StringBuilder和StringBuffer进行字符串追加操作,看下有何不同
我们做下简单的测试
首先,创建10个线程;
然后,每个线程循环100次往StringBuilder或者StringBuffer对象里面append字符。
我们预想结果是输出结果为1000,但是实际运行会输出什么呢?
2.1 StringBuilder测试
package com.joshua317;
public class StringBuilderTest {
public static void main(String[] args) throws InterruptedException {
StringBuilder stringBuilder = new StringBuilder();
for (int i=0; i<10; i++) {
(new Thread(new ThreadTestStringBuilder(stringBuilder))).start();
}
Thread.sleep(100);
System.out.println(stringBuilder.length());
}
}
class ThreadTestStringBuilder implements Runnable {
public StringBuilder stringBuilder;
ThreadTestStringBuilder(StringBuilder stringBuilder) {
this.stringBuilder = stringBuilder;
}
@Override
public void run()
{
for (int j=0; j < 100; j++) {
stringBuilder.append("a");
}
}
}
2.2 StringBuffer测试
package com.joshua317;
import java.util.Collection;
public class StringBufferTest {
public static void main(String[] args) throws InterruptedException {
StringBuffer stringBuffer = new StringBuffer();
for (int i=0; i<10; i++) {
(new Thread(new ThreadTestStringBuffer(stringBuffer))).start();
}
Thread.sleep(100);
System.out.println(stringBuffer.length());
}
}
class ThreadTestStringBuffer implements Runnable {
public StringBuffer stringBuffer;
ThreadTestStringBuffer(StringBuffer stringBuffer) {
this.stringBuffer = stringBuffer;
}
@Override
public void run()
{
for (int j=0; j<1000; j++) {
stringBuffer.append("a");
}
}
}
通过上面俩个例子,我们发现,StringBuilder在多线程执行的过程中,可能会出现字符长度小于1000,甚至出现了ArrayIndexOutOfBoundsException异常(异常非必现)。而StringBuffer则正常输出字符串长度为1000,从而我们可以简单得出StringBuilder是非线程安全的
三、分析StringBuilder执行所出现的问题
3.1 为什么输出值跟预期值不一样
我们先看一下StringBuilder的两个成员变量(这两个成员变量实际上是定义在AbstractStringBuilder里面的,StringBuilder和StringBuffer都继承了AbstractStringBuilder)
/**
* The value is used for character storage.
* 用于字符存储
*/
char[] value;
/**
* The count is the number of characters used.
* 用于记录字符数的数量
*/
int count;
再实例化StringBuilder时,我们可以看出,StringBuilder
默认的容量大小为16。当然也可以指定初始容量,或者以一个已有的字符序列给StringBuilder
对象赋初始值。
StringBuilder stringBuilder = new StringBuilder();
//StringBuilder类
public StringBuilder() {
super(16);
}
//AbstractStringBuilder类
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
再看StringBuilder的append方法
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
然后,StringBuilder的append方法调用了父类AbstractStringBuilder的append方法。
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
从代码count += len;
我们可以得知它不是一个原子操作。假设这个时候count值为100,len值为1,两个线程同时执行到了这一行,拿到的count值都是101,执行完加法运算后将结果赋值给count,所以两个线程执行完后count值为101,而不是102。这样就导致了字符的计数值要比我们预期结果1000小的原因。
而我们再来看下StringBuffer的append操作
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
StringBuffer 的append方法是synchronized 修饰的,保证了多线程情况下,同步操作。
3.2 为什么会抛出ArrayIndexOutOfBoundsException异常
我们看下AbstractStringBuilder的append()方法里面的ensureCapacityInternal()方法,它主要是用来检查StringBuilder对象的原char数组的容量能不能容纳下新的字符串,如果容纳不下就调用ensureCapacityInternal()方法对char数组进行扩容,也就是说StringBuilder 里面的ensureCapacityInternal()方法是用来扩展value数组的。
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value.length << 1) + 2;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
private int hugeCapacity(int minCapacity) {
if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
throw new OutOfMemoryError();
}
return (minCapacity > MAX_ARRAY_SIZE)
? minCapacity : MAX_ARRAY_SIZE;
}
//java.lang.Integer
@Native public static final int MAX_VALUE = 0x7fffffff;
//java.util.Arrays类中
public static char[] copyOf(char[] original, int newLength) {
char[] copy = new char[newLength];
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
//java.lang.System类中
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
扩容的方法最终是由expandCapacity()
实现的,在这个方法中首先把把容量变为原来容量的2倍加2,如果此时仍小于指定的容量,那么就把新的容量设为minimumCapacity
。然后判断是否溢出,如果溢出了,把容量设为MAX_ARRAY_SIZE
。最后通过Arrays.copyOf()方法调用System.arraycopy()方法,把value
值进行拷贝。
然后继续往下看代码,AbstractStringBuilder类的append()方法中str.getChars(0, len, value, count);
这一行作用是将String对象里面char数组里面的内容拷贝到StringBuilder对象的char数组里面
str.getChars(0, len, value, count);
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > value.length) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
我们用流程图来表示字符串拷贝的过程
1.假设现在有两个线程同时执行了StringBuilder的append()方法,两个线程都执行完了AbstractStringBuilder的ensureCapacityInternal()方法,此刻count=101。
2.此时线程1的cpu时间片用完了,线程2继续执行,线程2执行完整个append()方法后count变成102了。
3.线程1继续执行AbstractStringBuilder的str.getChars()方法的时候拿到的count值就是102了,执行char数组拷贝的时候就会抛出ArrayIndexOutOfBoundsException异常。
至此,StringBuilder执行所出现的问题已经分析完了,同时StringBuilder多线程不安全的问题也就迎刃而解。
四、扩展
4.1 什么是线程安全?
当多个线程访问某一个类(对象或方法)时,对象对应的公共数据区始终都能表现正确,那么这个类(对象或方法)就是线程安全的。通俗地理解就是:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
4.2 如何使用String、StringBuffer、StringBuilder呢?
String | StringBuffer | StringBuilder |
---|---|---|
不可变字符串 | 可变的字符序列 | 可变的字符序列 |
效率低 | 效率高 | |
线程安全 | 线程不安全 |
所以,
- 如果要操作少量的数据使用String;
- 多线程操作字符串缓冲区下操作大量数据使用StringBuffer;
- 单线程操作字符串缓冲区下操作大量数据使用StringBuilder。
记住,StringBuffer 适用于用在多线程操作同一个 StringBuffer 的场景,如果是单线程场合 StringBuilder 更适合。
4.3 具体的应用场景
-
在字符串内容不经常发生变化的业务场景优先使用String类。例如:常量声明、少量的字符串拼接操作等。
-
在频繁进行字符串的运算(如拼接、替换、删除等),并且运行在多线程环境下,建议使用StringBuffer,例如XML解析、HTTP参数解析与封装。
-
在频繁进行字符串的运算(如拼接、替换、删除等),并且运行在单线程环境下,建议使用StringBuilder,例如SQL语句拼装、JSON封装等。
4.4 为什么说StringBuilder是线程不安全的?
因为相对StringBuffer,StringBuilder没有在方法上使用 synchronized 关键字。Synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。至于如何分析,请参考上文。
本文为joshua317原创文章,转载请注明:转载自joshua317博客 https://www.joshua317.com/article/241