笔记之String、StringBuilder、StringBuffer的区别
1.安全性
- StringBuilder是线程不安全的,String、StringBuffer是线程安全性。
为什么这么说呢?
代码如下:
import java.util.concurrent.TimeUnit;
/**
* @author ql
* @version 1.0 2021/4/23
*/
public class StringDemo {
public static void main(String[] args) {
StringBuilder sb=new StringBuilder();
StringBuffer sbf=new StringBuffer();
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int j = 0; j < 10000; j++) {
sb.append("a");
sbf.append("a");
}
}).start();
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("StringBuilder添加后的长度:"+sb.length());
System.out.println("StringBuffer添加后的长度:"+sbf.length());
}
}
运行结果
StringBuilder可能会爆的异常:Exception in thread "Thread-1"
java.lang.ArrayIndexOutOfBoundsException
StringBuilder添加后的长度:99709
StringBuffer添加后的长度:100000
在上述的运行结果中,我们能看到这段代码创建了10个线程,每个线程循环10000次分别往StringBuilder、StringBuffer对象里面append字符。正常情况下代码应该输出100000的。但是为什么只有StringBuffer的对象的长度为100000,而StringBuilder的长度小于10000,并且StringBuilder还抛出了一个ArrayIndexOutOfBoundsException异常(数组索引超出范围异常 )(不过这个异常不是一定会出现的,我试了几次才出现)。
1.1 为什么输出值跟预期值不一样
我们先看一下StringBuilder、StringBuffer的两个成员变量(这两个成员变量实际上是定义在AbstractStringBuilder里面的,StringBuilder和StringBuffer都继承了AbstractStringBuilder)
public final class StringBuffer extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
public final class StringBuilder extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
再看StringBuffer的append()方法:
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
再看StringBuilder的append()方法:
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
StringBuffer、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值为10,len值为1,两个线程同时执行到了第七行,拿到的count值都是10,执行完加法运算后将结果赋值给count,所以两个线程执行完后count值为11,而不是12。这就是为什么测试代码输出的值要比10000小的原因。
- 而为什么StringBuffer同样也是调用了父类AbstractStringBuilder的append()方法,但是输出的结果正常呢?在于StringBuffer的append方法添加锁synchronized ,这样就导致了调用父类AbstractStringBuilder的append()方法的时候,也会添加锁,同一时间只有一个线程可以进行字符串的添加操作,所以最后的结果正常。
1.2 存储的方式
由下面的代码可以看到String存储数据的底层是char 数组,并且其是被final修饰,表示不可变的,而因为StringBuilder,StringBuffer都是继承AbstractStringBuilder,所以其存储数据的底层也是char 数组,但是,其没有被final修饰,所以StringBuilder,StringBuffer都是可变的。
String中的
//存储字符串的具体内容
private final char value[];
AbstractStringBuilder中:
//存储字符串的具体内容
char[] value;
//已经使用的字符数组的数量
int count;
1.3 StringBuilder为什么会抛出ArrayIndexOutOfBoundsException异常?
我们看回AbstractStringBuilder的append()方法源码的第五行,ensureCapacityInternal()方法是检查StringBuilder对象的原char数组的容量能不能盛下新的字符串,如果盛不下就调用expandCapacity()方法对char数组进行扩容。
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
- 扩容的逻辑就是new一个新的char数组,新的char数组的容量是原来char数组的两倍再加2。在把扩容后的数组返回去,再通过Arrays.copyOf()函数将原数组的内容复制到新数组,最后将指针指向新的char数组。
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;
}
Arrys.copyOf()方法
public static char[] copyOf(char[] original, int newLength) {
char[] copy = new char[newLength];
//System拷贝数组
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
AbstractStringBuilder的append()方法源码的第六行,是将String对象里面char数组里面的内容拷贝到StringBuilder对象的char数组里面,代码如下:
str.getChars(0, len, value, count);
getChars()方法
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);
}
拷贝流程见下图:
假设现在有两个线程同时执行了StringBuilder的append()方法,两个线程都执行完了第五行的ensureCapacityInternal()方法,此刻count=5。
这个时候线程1的cpu时间片用完了,线程2继续执行。线程2执行完整个append()方法后count变成6了。
线程1继续执行第六行的str.getChars()方法的时候拿到的count值就是6了,执行char数组拷贝的时候就会抛出ArrayIndexOutOfBoundsException异常(Array Index OutOf BoundsException)。
1.4 String的安全性
- 同样String的操作中虽然没有添加锁的,但是final修饰的String,代表了String的不可继承性,final修饰的char[]代表了被存储的数据不可更改性。
一、Java String类为什么是final的?
1.为了实现字符串池
2.为了线程安全
3.为了实现String可以创建HashCode不可变性
二、Java final的用途?
1、final可以修饰类,方法和变量,
2、final修饰的类,不能被继承,即它不能拥有自己的子类,
3、final修饰的方法,不能被重写,
4、final修饰的变量,无论是类属性、对象属性、形参还是局部变量,都需要进行初始化操作。
注: final代表了不可变,但仅仅是引用地址不可变,并不代表了数组本身不会变
final也可以将数组本身改变的,这个时候,起作用的还有private,正是因为两者保证了String的不可变性。
那么为什么保证String不可变呢?
因为只有当字符串是不可变的,字符串池才有可能实现。
字符串池的实现:可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。
因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。
因为字符串是不可变的,所以在它创建的时候HashCode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。
1.5 SpringBuffer与StringBuilder
因为SpringBuffer
和StringBuilder
都是继承AbstractStringBuilder
类,并且去除了个别方法,大部分都是通过super.xxx
来调用AbstractStringBuilder
的方法来实现的。
还有就是因为SpringBuffer
更多考虑的是并发下的大数据量的操作,所以其对字符串进行修改的方法都用synchronized
进行修饰。
还有就是SpringBuffer
中的方法中比StringBuilder
多了toStringCache
的变量,其中toStringCache
是字符数组 value
复制的一个副本,每当 value
发生改变时,toStringCache
都会被置为空。
这就保证了每次只要 StringBuffer
对象发生改变,再调用 toString()
方法就必然产生一个新的 toStringCache
数组,从而保证了引用了旧的 toStringCache
的字符串对象不会发生改变。
即使多个线程同时访问 StringBuffer
对象,某一时刻也只有一个线程能够进入修改 toStringCache
和 value
的代码块,这通过修饰 StringBuffer
方法的 synchroinzed
关键字来保证。
@Override
public synchronized String toString() {
if (toStringCache == null) {
toStringCache = Arrays.copyOfRange(value, 0, count);
}
return new String(toStringCache, true);
}
那么 StringBuffer
中 toStringCache
存在的必要性如何?它调用的是下面这个构造方法来创建 String 对象,构造 String 对象时直接共享传入的字符数组 value
,而不是像 public String(char value[])
一样复制一份。
/*
* Package private constructor which shares value array for speed.
* this constructor is always expected to be called with share==true.
* a separate constructor is needed because we already have a public
* String(char[]) constructor that makes a copy of the given char[].
*/
String(char[] value, boolean share) {
// assert share : "unshared not supported";
this.value = value;
}
从源码中可以知道,StringBuffer
中使用 toStringCache
通过共享一个字符数组,提供构造 String 的速度,这是一个好处。另一个好处是连续多次调用 toString()
方法是不会产生多个内容相同的 String 对象。
但是,这些好处仅仅是在多次调用 toString()
方法且 StringBuffer
对象没有发生改变时才能体现。而实际编写代码的过程中,很少会在没有修改 StringBuffer
的情况下重复调用 toString()
方法,所以它并没有太大的实际作用。
2. 总结一下:
-
String 类型和 StringBuffer 的主要性能区别:String 是不可变的对象, 因此在每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象,所以经常改变内容的字符串最好不要用 String ,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后, JVM 的 GC 就会开始工作,性能就会降低。
-
StringBuffer相对于StringBuilder效率要相对低一点,但也远比String要高的多。效率低的原因:对于StringBuffer来说更多的考虑到了多线程的情况,在进行字符串操作的时候,它使用了synchronized关键字,对方法进行了同步处理。因此StringBuffer适用于多线程环境下的大量操作。
-
在进行多线程处理的时候,如果多个线程对于这一个对象同时产生操作,会产生预期之外的结果。对于StringBuilder来说,执行效率虽然高,但是因为线程不安全,所以不建议在多线程的环境下对同一个StringBuilder对象进行操作。因此StringBuilder适用于单线程环境下的大量字符串操作。
-
StringBuilder和StringBuffer的初始化容量都是16,扩展的话,每次是2倍加2.
public StringBuilder() {
super(16);
}
public StringBuffer() {
super(16);
}
int newCapacity = (value.length << 1) + 2;
String | StringBuffer | StringBuilder | |
---|---|---|---|
执行速度 | 最差 | 其次 | 最高 |
线程安全 | 线程安全 | 线程安全 | 线程不安全 |
使用场景 | 少量字符串操作 | 多线程环境下的大量操作 | 单线程环境下的大量操作 |
文章参考:https://blog.csdn.net/kingzone_2008/article/details/9220691
https://www.runoob.com/w3cnote/java-different-of-string-stringbuffer-stringbuilder.html
https://blog.csdn.net/Turniper/article/details/111112824
为什么 StringBuffer 有 toStringCache 而 StringBuilder 没有?