臭名昭著aTrust隔离计划1.臭名昭著aTrust隔离计划2.VPN原理与aTrust隔离网络实践3.docker-easyconnect到底做了什么4.TUN(tunnel-隧道-虚拟网卡)模式
个人项目
personal(个人展示)GitHub Actions 自动部署个人展示项目
基于Quartz搭建的个人博客1.Quartz个人博客使用教程2.使用 rsync 增量部署 Quartz 博客3.使用 GitHub Actions 自动部署 Quartz 博客5.域名绑定6.CDN加速7.Google 搜索收录排查:为什么关键词搜得到别人,却搜不到我的网站?Obsidian、双向链接与知识图谱
架构设计
分布式分布式事务RPC原理
旅行
行程攻略青岛三日游行程攻略
前端
NginxNginx配置与反向代理入门
Nodenpm与npx的区别
数据库
mysql01.MySQL基础语法与约束02.MySQL数据库类型03.MySQL多表查询04.MySQL 索引05.MySQL 事务06.MySQL 隔离级别07.MySQL MVCC08.MySQL 锁09.EXPLAIN10.慢查询与 SQL 优化
mysql面试题01.索引问题02.事务与隔离级别03.锁机制04.存储引擎与性能优化05.用户权限、字符集与备份恢复
mysql实战01.EXPLAIN实战02.测试环境菜单误删排查与恢复03.MySQL IN 详解
网络
网络安全1.无线监听网卡苹果M系列虚拟机安装Kali Linux教程3.kali怎么使用RTL8812AU4.kali基础命令
网络基础进程之间怎样通过网络传递数据
网址常用网址
中间键
esElasticsearch 入门
mq
RabbitMQRabbitMQ高级RabbitMQ基础
1. MQ 基础2. MQ 核心角色与代码对应3. MQ Consumer Group 与消费模式4. SOFAMQ 账户变更代码实践5. MQ 可靠性问题6. 消息幂等
redis缓存击穿、穿透与雪崩Redis 分布式锁Redis 缓存问题Redis 数据结构
AI
使用说明
第三方插件&技能简介Archify使用与安装指南geekgeekRun使用OfferFlow使用指南Ponytail使用指南Skills Manager使用指南
CodexCodex CLI与IDE区别及使用指南Codex Hook单独配置与提交通知Codex MCP安装与使用指南Codex第三方插件安装与使用指南
Agent开发0-学习路线1.LLM、Token、上下文窗口与模型参数2.大模型 API、请求参数与响应结构3.Spring AI ChatClient4.Prompt、System Prompt、Prompt 模板6.结构化输出、JSON Schema7.SSE 流式响应8.会话 ID、聊天记录、Redis9.超时、重试、限流、降级10.*完成可运行聊天接口11.Function Calling、Tool Calling 原理12.用 Java 方法定义 Tool13.工具参数设计和校验14.实现订单查询工具15.实现物流查询工具16.实现库存查询工具17.多个工具自动选择18.工具异常、超时、重试
diy主机
主机零件学习1.CPU2.主板3.内存4.显卡5.硬盘6.散热7.风扇8.机箱热门型号价格表
捡垃圾diy-13600kf+5060diy-14600kf+5070
English高频词汇英文基础语法简单句子音标
GitGit常用命令Sublime Merge 使用指南
Java
工具库Caffeine本地缓存Reactor教程
工作实践如何使用@Async线程池使用与排查线上服务 CPU 使用率过高排查压测下RPC接口大量失败排查记录
基础1.HashMap2.ConcurrentHashMap3.HashSet4.final5.static6.volatile7.AtomicInteger8.String动态代理反射锁与线程同步线程注解
面试题1.HashMap 存储在方法内是线程安全的吗2.ArrayList参数传递与引用重新赋值3.== 和 equals 的区别、hashcode4.重载和重写的区别5.String、StringBuffer、StringBuilder区别及使用场景6.接口和抽象类7.ArrayList 与 LinkedList 的区别和扩容机制8.HashMap 和 Hashtable 的区别及底层实现9.Object 和 Objects 的区别10.ThreadLocal的原理和使用场景11.字节码是什么及作用12.Java类加载器有哪些13.双亲委派模型14.Java异常体系15.GC是什么16.GC如何判断对象可以被回收
面试总结0.HR面试模板1.面试一2.面试二3.面试三笔试
网络与 IO(黑马程序员-纯理论)01-网络编程基础02-IO模型与零拷贝03-BIO04-NIO基础与缓冲区05-NIO通道与选择器06-AIO
juc00-目录01-进程与线程02-线程同步与通信03-并发设计模式04-Java内存模型05-CAS与原子类06-ThreadLocal07-阻塞队列08-线程池使用与原理09-任务调度10-ForkJoin与享元模式11-AQS12-ReentrantLock13-读写锁14-并发协作工具15-ConcurrentHashMap16-CopyOnWrite与同步容器17-SkipListMap18-非阻塞队列
juc(工作常见)Java并发编程三个重点拆解
jvm1. JVM运行时数据区
Spring1.Spring IoC、DI与构造方法注入CGLIB、Byte Buddy 与 Spring AOPSpring 常用注解详解Spring Bean 生命周期
Spring MVC异常处理机制
linux常用命令
mac快捷键终端常用操作
python
基础01.Python是什么,为什么要安装Python02.Python项目中.venv是什么03.Python基础语法
基础实战sql替换
HTTPXHRMS员工详情接口调用(Python HTTPX)
windowswindows激活

一、💡 一句话理解

核心结论

String 内容不可变,适合保存普通字符串;StringBuilder 可变且速度通常更快,适合单线程频繁拼接;StringBuffer 可变且方法带同步,适合多个线程共享同一个拼接对象。

记忆:普通字符串用 String,单线程拼接用 Builder,多线程共享拼接才考虑 Buffer。

二、🧭 理论:它们是什么

2.1 核心区别

对比项StringStringBuilderStringBuffer
内容能否修改不能,修改会产生新字符串能,在原对象上修改能,在原对象上修改
线程安全内容不可变,可安全共享不保证线程安全单个方法使用同步保证线程安全
拼接性能少量拼接够用,循环拼接成本较高通常最快因同步开销,通常比 StringBuilder 慢
常见场景常量、参数、返回值、少量拼接单线程循环拼接、组装 SQL 或文本多线程确实共享同一个字符缓冲区

三、⚙️ 理论:它们怎么工作

3.1 String:内容不可变

String 对象创建后,其内容不能改变。下面的 + 并没有修改原字符串,而是产生了新的字符串对象,再让变量指向新对象。

String text = "Java";
text = text + "面试"; // 得到新的字符串 "Java面试"

因此,循环中反复使用 + 可能创建较多临时对象。不过,简单表达式可能被编译器优化,不必把所有 + 都改成 StringBuilder。

3.2 StringBuilder 与 StringBuffer:内容可变

它们内部维护可扩容的字符序列,调用 append() 时可以在同一个对象上继续追加内容。

两者的主要区别是:

  • StringBuilder 不提供同步保证,通常性能更好。
  • StringBuffer 的主要操作使用同步,允许多个线程操作同一个实例。

使用边界

StringBuffer 只能保证单次方法调用的线程安全。多个调用组合成一个完整业务操作时,仍可能需要额外同步。实际开发中,如果拼接对象只是方法内的局部变量,通常直接使用 StringBuilder。

四、🚀 实践:从选择到验证

4.1 可以拿来干什么

  • 字符串固定或只做少量拼接:使用 String。
  • 在一个线程中循环组装日志、报文或 SQL:使用 StringBuilder。
  • 多个线程必须共享并修改同一个字符序列:考虑 StringBuffer,同时检查是否存在组合操作。

4.2 完整实践

文件:StringDemo.java

public class StringDemo {
    public static void main(String[] args) {
        // String:适合保存最终结果或进行少量拼接。
        String title = "Java" + "面试";
 
        // StringBuilder:当前对象只在 main 线程中使用,适合循环追加。
        StringBuilder builder = new StringBuilder();
        for (int i = 1; i <= 3; i++) {
            // append:把内容追加到同一个可变字符序列,并返回 builder 自身。
            builder.append("第").append(i).append("题");
 
            if (i < 3) {
                builder.append("、");
            }
        }
 
        // toString:把可变字符序列转换成最终的不可变 String。
        String result = builder.toString();
 
        System.out.println(title);
        System.out.println(result);
    }
}

运行:

javac StringDemo.java
java StringDemo

预期输出:

Java面试
第1题、第2题、第3题

五、🎤 面试时怎么流利地说

5.1 回答路线

先说是否可变,再说线程安全和性能,最后给出使用场景。

5.2 30 秒标准回答

String 是不可变对象,每次看似修改字符串,实际通常会产生新的字符串,所以适合保存固定内容和少量拼接。StringBuilder 和 StringBuffer 都是可变字符序列,适合频繁追加;区别是 StringBuilder 不保证线程安全,通常更快,适合单线程或方法内的局部变量;StringBuffer 的方法带同步,适合多个线程共享同一个实例。实际开发中一般优先使用 String 或 StringBuilder,只有确实需要共享可变字符序列时才考虑 StringBuffer。

5.3 高频追问与短答

  • **字符串使用 + 一定很慢吗?**不一定。简单拼接可能被编译器优化;需要重点避免的是循环中反复拼接。

可以把“线程安全”拆成两件事理解:对象内部会不会被改坏,以及一连串操作会不会被其他线程插队。

5.3.1 String:内容不会被改坏,但共享变量仍可能出错

String 创建后内容不能修改:

String text = "Java";
text = text + "面试";

第二行不是把原来的 "Java" 改成 "Java面试",而是创建一个新字符串,再让变量 text 指向它。因此,多个线程只读取同一个 String 对象时,不会把字符串内容改乱,这种共享是安全的。

但如果多个线程同时修改同一个共享变量:

static String text = "";
 
// 两个线程都执行这一行。
text = text + "A";

这一行可以拆成三个步骤:

  1. 读取 text 的旧值。
  2. 根据旧值创建一个新字符串。
  3. 让 text 指向新字符串。

假设线程 1 和线程 2 同时读到旧值 "",它们都会生成 "A",然后分别给 text 赋值。两个线程虽然都执行了一次追加,但最终结果可能只是 "A",其中一次修改被覆盖了。

怎么理解

String 保证的是“字符串对象的内容不会被修改”,不保证“读取共享变量、拼接、重新赋值”这一整套操作不会被其他线程插队。

5.3.2 StringBuffer:单个方法安全,不代表一连串操作都安全

StringBuffer 的 append()、length()、deleteCharAt() 等方法内部使用了同步。多个线程同时调用其中一个方法时,会排队执行,不会一起修改内部数据。

但是,下面的代码包含两个独立的方法调用:

if (buffer.length() > 0) {
    buffer.deleteCharAt(0);
}

可能出现下面的执行顺序:

  1. 线程 1 调用 length(),发现长度大于 0。
  2. 线程 1 还没删除字符,线程 2 先把 buffer 清空了。
  3. 线程 1 继续调用 deleteCharAt(0),此时已经没有字符,可能发生越界异常。

虽然 length() 和 deleteCharAt() 单独调用时都是线程安全的,但两次调用之间仍然可能被其他线程插队。如果业务要求“判断和删除必须连续完成”,就要把整个操作一起加锁:

synchronized (buffer) {
    if (buffer.length() > 0) {
        buffer.deleteCharAt(0);
    }
}

怎么理解

StringBuffer 保证的是“每个方法单独排队执行”;多个方法组成一个不可分割的业务操作时,仍然需要根据情况在外层统一加锁。

六、📌 总结

  • String:不可变,适合普通字符串。
  • StringBuilder:可变、无同步,适合单线程频繁拼接。
  • StringBuffer:可变、有同步,适合多线程共享实例。
  • 日常优先级通常是:String → StringBuilder → 确有共享并发需求时使用 StringBuffer。

七、🔗 官方资料