Java基础:String、StringBuilder 和 StringBufferr对比
目录
基础用法
1.String
2.StringBuilder和StringBufferr
略微深入
1.为什么StringBuiler线程不安全
2.为什么StringBuffer线程安全
基础用法
1.String
在Java中,String是不可变类。
所以new一个String对象之后,它的值是不可变的。对它的修改,实际上是生成新对象。
例如:
public class StringExample { public static void main(String[] args) { String str1 = "Hello"; String str2 = str1.concat(", World!"); System.out.println(str1); // 输出:Hello System.out.println(str2); // 输出:Hello, World! } }2.StringBuilder和StringBufferr
二者都是可变类,也就是能在不生成新对象的情况下修改字符串。单纯从使用角度来看是差不多的
例如:
public class StringBuilderExample { public static void main(String[] args) { StringBuilder sb = new StringBuilder("Hello"); sb.append(", World!"); System.out.println(sb.toString()); // 输出:Hello, World! } }略微深入
三者之间的区别很直观,如下表所示。接下来主要基于具体问题,深入源码分析
1.为什么StringBuiler线程不安全
StringBuilder 和 StringBuffer,二者都继承父类 AbstractStringBuilder。
对象内部核心就两个成员:
char[] value; // 真正存放字符的数组 int count; // 记录数组里实际存了多少个有效字符StringBuilder中的append,直接调用父类:
@Override public StringBuilder append(String str) { // 直接调用父类 AbstractStringBuilder 的append super.append(str); return this; }再看父类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位置开始写入 count += len; // 计数器自增 return this; }重点关注计数器自增操作,这也是导致线程不安全的原因:
问题 1:count += len 非原子操作
count += len 会拆分成 3 步 CPU 指令:
读取 count 当前值;
执行加法运算;
将结果写回 count 变量。
如果多线程同时执行:
线程 A、线程 B 读到同一个 count 值,加法完成后先后写回,后写入的值会覆盖前一个,最终 count 偏小,字符丢失。
问题 2:ensureCapacityInternal 并发判断产生越界
线程 A、线程 B 同时判断,都认为容量充足,不触发扩容。
此时线程 A先拼接,线程B后拼接,一起拼进去长度可能就超出数组范围了,会抛出 ArrayIndexOutOfBoundsException。
2.为什么StringBuffer线程安全
看StringBuffer的append方法:
@Override public synchronized StringBuffer append(String str) { super.append(str); return this; }好像和StringBuilder差不多?
关键点在于多了synchronized修饰:
synchronized是 Java 内置的悲观锁,JVM 层面实现,用来保证多线程并发下:
原子性:代码块一次性执行完毕,不会被线程打断
可见性:一个线程修改变量,其他线程立刻可见
有序性:禁止指令重排
自然也就不会出现和StringBuilder一样的问题,因为多个线程访问同一个对象的同一方法会互斥。
