乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Java概述与环境搭建

    • Java概述与环境搭建
    • Java语言概述
    • 解释型语言与编译型语言对比
    • JDK安装与配置
    • JDK、JRE、JVM 详解
    • HelloWorld程序详解
    • IDE 介绍
  • 第2章 标识符与基本数据类型

    • 章节导读
    • 变量概述
    • 常量概述
    • 基本类型与包装类
    • 字节型 byte
    • 短整型 short
    • 整型 int
    • 长整型 long
    • 单精度浮点型 float
    • 双精度浮点型 double
    • 字符型 char
    • 布尔型 boolean
    • 类型转换
  • 第3章 运算符与表达式

    • 章节导读
    • 算术运算符
    • 赋值运算符
    • 关系运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 运算符优先级
    • 表达式
  • 第4章 流程控制

    • 章节导读
    • 常见的程序运行流程
    • if-else 选择结构
    • switch 多分支选择
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
  • 第5章 数组

    • 章节导读
    • 一维数组
    • 多维数组
    • Arrays 工具类
  • 第6章 类与对象

    • 章节导读
    • 类与对象
    • 方法定义与调用
    • 构造方法
    • 封装
    • 访问修饰符
    • package 与 import
    • static 关键字
    • this 关键字
    • 参数传递 详解
    • 枚举
    • 成员内部类
    • 局部内部类
    • 静态内部类
    • 匿名内部类
  • 第7章 接口与继承

    • 章节导读
    • 继承
    • super 关键字
    • final 关键字
    • 多态
    • 向上转型与向下转型
    • 抽象类
    • 接口
    • 抽象类与接口对比
  • 第8章 注解

    • 章节导读
    • 注解基础
    • 元注解详解
    • 自定义注解
  • 第9章 常用类

    • 章节导读:Java 常用类
    • Object 类:万类之祖
    • 包装类:基本类型的对象化
    • String:不可变的字符串
    • StringBuffer:线程安全的可变字符串
    • StringBuilder:可变的字符串构建器
    • Math:数学运算工具类
    • Random:伪随机数生成器
    • 大数值运算 详解
    • 日期时间API 详解
  • 第10章 异常机制

    • 章节导读
    • 异常体系与分类
    • try-catch-finally
    • try-with-resources
    • throws 与 throw
    • 自定义异常
  • 第11章 泛型

    • 章节导读
    • 泛型基础
    • 通配符与PECS原则
    • 类型擦除
  • 第12章 集合框架

    • 章节导读
    • 集合框架概述
    • ArrayList
    • LinkedList
    • HashMap 详解
    • LinkedHashMap 详解
    • TreeMap 详解
    • HashSet
    • TreeSet 详解
    • TreeSet 与 Comparable
    • Collections 工具类详解
  • 第13章 IO流

    • 章节导读
    • IO流概述
    • 字节流
    • 字符流
    • 缓冲流
    • 转换流 详解
    • 序列化 详解
    • NIO与Files 详解
    • NIO与Files工具类
  • 第14章 多线程与并发

    • 第十六章 多线程与并发 —— 章节导读
    • 线程基础详解
    • synchronized 详解
    • Lock 与显式锁详解
    • volatile 详解
    • wait 与 notify 详解
    • ThreadLocal详解
    • 原子类详解
    • 并发工具类详解
    • 线程池详解
  • 第15章 反射

    • 章节导读
    • 反射概述与 Class 对象
    • Constructor 与对象创建
    • Field与Method详解
    • 反射应用详解
  • 第16章 JDK8新特性

    • 章节导读
    • Lambda 表达式
    • Stream API 基础
    • Stream API 高级详解
    • Optional 详解
    • 新日期时间API详解
  • 第17章 JDK9-11新特性

    • 章节导读
    • 模块化系统 — Project Jigsaw(JDK 9)
    • var 局部变量类型推断(JDK 10)
    • 集合工厂方法与增强(JDK 9 / 10 / 11)
    • 接口增强:private 方法(JDK 9)
    • Stream API 增强(JDK 9)
    • Optional 增强(JDK 9 / 10 / 11)
    • String 新增方法(JDK 11)
    • HTTP Client 与 Files 增强(JDK 11)
    • 直接运行 Java 源文件 — JEP 330(JDK 11)
  • 第18章 JDK12-17新特性

    • 章节导读
    • Switch 表达式(JDK 12 预览 / JDK 14 正式)
    • 文本块 Text Blocks(JDK 13 预览 / JDK 15 正式)
    • Records 记录类(JDK 14 预览 / JDK 16 正式)
    • 密封类 Sealed Classes(JDK 15 预览 / JDK 17 正式)
    • instanceof 模式匹配(JDK 14 预览 / JDK 16 正式)
    • Switch 模式匹配 — Pattern Matching for switch(JDK 17 预览 / JDK 21 正式)
    • Helpful NPE 与 String 增强(JDK 12 / JDK 14 / JDK 15)
    • Stream 增强(JDK 12 / JDK 16)
    • 日期时间增强 — Day Period 支持(JDK 16)
  • 第19章 JDK18-21新特性

    • 章节导读
    • 虚拟线程(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 序列集合(JDK 21 正式)
    • Switch 模式匹配(JDK 17 预览 / JDK 18 第二预览 / JDK 20 第四预览 / JDK 21 正式)
    • Record 模式匹配(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 未命名模式与变量(JDK 21 预览 / JDK 22 正式)
  • 第20章 JDK 22-25 新特性

    • 章节导读
    • 字符串模板(JDK 22 预览 / JDK 23 第二预览 / JDK 24 第三预览)
    • Stream Gatherers(JDK 22 预览 / JDK 24 第二预览)
    • 隐式声明类与实例方法(JDK 23 预览 / JDK 24 第二预览)
    • 原始类型模式匹配(JDK 24 预览)
  • 附录

    • Java 核心知识点
    • Java SE 专业术语
    • Java特性索引(JDK 8 → 25)

StringBuffer:线程安全的可变字符串

本章定位:StringBuffer 是 StringBuilder 的线程安全版本,所有公开方法都用 synchronized 修饰。当多个线程需要操作同一个字符串缓冲区时(如日志收集器、共享缓冲区),它是必需的选择。


孔蓝的并发测试

飞翔科技的日志收集模块在生产环境偶尔出现日志丢失。孔蓝(测试工程师)写了一个并发测试来复现:

// 原来的代码:使用 StringBuilder 作为全局日志缓冲
public class Logger {
    private static StringBuilder logBuffer = new StringBuilder();

    public static void log(String msg) {
        logBuffer.append(msg).append("\n");  // 多线程写入!
    }
}
// 启动 100 个线程同时写日志 → 最终长度不足预期(数据丢失)

白歌看后说:"这是典型的竞态条件。StringBuilder 的 append 不是原子操作——修改 count 和写入 value 数组之间可能被其他线程打断。你需要 StringBuffer。"


StringBuilder vs StringBuffer

核心对比

维度StringBuilderStringBuffer
引入版本JDK 5JDK 1.0
线程安全不安全安全(synchronized)
性能高较低(锁开销)
适用场景单线程(局部变量、方法内)多线程共享
继承关系AbstractStringBuilderAbstractStringBuilder
几乎所有方法无 synchronized带 synchronized

StringBuilder 和 StringBuffer 都继承自 AbstractStringBuffer(这是一个包私有的抽象类,位于 java.lang 包)。两者 API 几乎完全一致——唯一的本质区别就是 StringBuffer 在每个公开方法上加上了 synchronized 关键字。

所有公开方法都加锁

打开 JDK 源码可以看到类似这样的实现:

// StringBuffer.java 源码片段
@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

@Override
public synchronized StringBuffer insert(int offset, String str) {
    toStringCache = null;
    super.insert(offset, str);
    return this;
}

完整示例一:线程安全的日志收集器

场景

孔蓝重新设计了飞翔科技的日志收集模块,使用 StringBuffer 作为多线程共享的缓冲区。

代码

public class StringBufferThreadSafeDemo {
    public static void main(String[] args) throws InterruptedException {
        StringBuffer sharedBuffer = new StringBuffer();  // 线程安全

        // 启动 10 个线程,每个追加 1000 个字符
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            final int id = i;
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    sharedBuffer.append(id % 10);  // synchronized 保证原子性
                }
            });
            threads[i].start();
        }

        for (Thread t : threads) {
            t.join();  // 等待所有线程完成
        }

        System.out.println("最终长度: " + sharedBuffer.length());  // 应该是 10000
        // 如果用 StringBuilder,长度可能 < 10000(数据丢失)
    }
}

输出

最终长度: 10000

如果将此处的 StringBuffer 换成 StringBuilder,最终长度大概率不足 10000——因为多个线程同时修改 count 字段和 value 数组,出现了竞态条件(Race Condition)。


完整示例二:飞翔科技多线程日志系统

场景

白歌让孔蓝构建一个完整的飞行日志系统:多个服务线程同时写日志,主线程定期读取并清空缓冲区。

代码

public class ThreadSafeLogger {
    // 使用 StringBuffer 作为线程安全的日志缓冲区
    private static final StringBuffer logBuffer = new StringBuffer(8192);

    public static void main(String[] args) throws InterruptedException {
        System.out.println("===== 飞翔科技 多线程日志系统 =====\n");

        // ========== 启动 5 个模拟服务线程 ==========
        Thread[] services = new Thread[5];
        String[] serviceNames = {"订单服务", "支付服务", "库存服务", "用户服务", "通知服务"};

        for (int i = 0; i < 5; i++) {
            final String name = serviceNames[i];
            services[i] = new Thread(() -> {
                for (int j = 0; j < 10; j++) {
                    log("[INFO] " + name + " - 处理第 " + (j + 1) + " 条请求");
                    try {
                        Thread.sleep((long) (Math.random() * 10));  // 模拟处理时间
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            }, name);
            services[i].start();
        }

        // ========== 主线程定期输出日志 ==========
        for (int round = 0; round < 3; round++) {
            Thread.sleep(50);  // 等待 50ms 收集一批日志
            flushLogs();
        }

        // ========== 等待所有服务线程结束 ==========
        for (Thread t : services) {
            t.join();
        }
        flushLogs();  // 最后一批

        System.out.println("\n所有服务线程已完成。日志系统关闭。");
    }

    // 线程安全地读取并清空日志
    private static synchronized void flushLogs() {
        if (logBuffer.length() > 0) {
            System.out.println("--- 日志刷新 (" + logBuffer.length() + " 字符) ---");
            System.out.print(logBuffer.toString());
            logBuffer.setLength(0);  // 清空缓冲区(线程安全)
        }
    }

    // 日志写入方法
    private static void log(String message) {
        logBuffer.append(System.currentTimeMillis() % 100000)
                 .append(" [")
                 .append(Thread.currentThread().getName())
                 .append("] ")
                 .append(message)
                 .append("\n");
    }
}

运行输出

===== 飞翔科技 多线程日志系统 =====

--- 日志刷新 (1234 字符) ---
47581 [订单服务] [INFO] 订单服务 - 处理第 1 条请求
47582 [支付服务] [INFO] 支付服务 - 处理第 1 条请求
47582 [库存服务] [INFO] 库存服务 - 处理第 1 条请求
47583 [用户服务] [INFO] 用户服务 - 处理第 1 条请求
47583 [通知服务] [INFO] 通知服务 - 处理第 1 条请求
47584 [订单服务] [INFO] 订单服务 - 处理第 2 条请求
...

--- 日志刷新 (1456 字符) ---
...

所有服务线程已完成。日志系统关闭。

深度分析:synchronized 的粒度问题

StringBuffer 的单个方法(如 append())是原子的,但复合操作不是原子的。例如:

// ⚠️ 看似安全,实际不是!
StringBuffer sb = new StringBuffer();
// 线程 A: sb.append("Hello-");
// 线程 B: sb.append("World-");
// 可能出现: "Hello-World-" ✓(A 先完成,B 后完成)
//       或: "World-Hello-" ✓(B 先完成,A 后完成)
// 不会出现: "HeWlloor-ld-" ✗(单次 append 是原子的,字符不会交错)

但如果用 StringBuilder:

// ❌ 使用 StringBuilder 时,单次 append 也可能交错
StringBuilder sb2 = new StringBuilder();
// 线程 A: sb2.append("Hello-");
// 线程 B: sb2.append("World-");
// 可能出现: "HeWlloor-ld-"(字符交错!因为 count 和 value 的修改被打断)

然而,即使是 StringBuffer,复合操作仍需外部同步:

// ⚠️ 即使是 StringBuffer,这两行之间也可能被其他线程插入
sb.append(userName);
sb.append(" - ");  // 另一个线程可能在这两行之间 append 了内容

易错场景

反例一:多线程下使用 StringBuilder

// ❌ 错误:多线程共享 StringBuilder 导致数据混乱
public class Logger {
    private static StringBuilder log = new StringBuilder();  // 单例,多线程共享!

    public static void info(String msg) {
        log.append("[INFO] ").append(msg).append("\n");  // 线程不安全!
    }
}

纠正:

// ✅ 正确一:改用 StringBuffer
public class Logger {
    private static StringBuffer log = new StringBuffer();

    public static void info(String msg) {
        log.append("[INFO] ").append(msg).append("\n");  // 单次 append 线程安全
    }
}

// ✅ 正确二:局部变量使用 StringBuilder(推荐,线程封闭)
public class Logger {
    public static void info(String msg) {
        StringBuilder sb = new StringBuilder();  // 局部变量,线程封闭
        sb.append("[INFO] ").append(msg);
        System.out.println(sb.toString());
    }
}

反例二:误以为 StringBuffer 所有操作序列都线程安全

// ❌ 错误:复合操作不加外部同步
private static StringBuffer report = new StringBuffer();
public static void addSection(String title, String body) {
    report.append("=== ").append(title).append(" ===\n");
    report.append(body).append("\n\n");  // 这两次 append 之间可能被打断!
}

纠正:

// ✅ 正确:复合操作用外部 synchronized 保护
private static StringBuffer report = new StringBuffer();
public static synchronized void addSection(String title, String body) {
    report.append("=== ").append(title).append(" ===\n");
    report.append(body).append("\n\n");
}

面试考点

Q1:StringBuilder 和 StringBuffer 的区别是什么?如何选择?

核心区别是线程安全:StringBuffer 所有 public 方法都加了 synchronized,线程安全但开销大;StringBuilder 没有同步,性能更高。单线程环境(如方法内部局部变量、循环拼接)应使用 StringBuilder;多线程共享的字符串缓冲区才需要 StringBuffer。绝大多数情况下 StringBuilder 是正确选择。

Q2:StringBuffer 的 synchronized 能保证所有操作都线程安全吗?

单个方法(如 append())是原子的,但复合操作不是。例如 sb.append(a).append(b) 拆成两次调用,中间可能被其他线程插入。如果需要原子性的复合操作,需要在外部加 synchronized 块保护整个操作序列。

Q3:为什么说"循环中用 + 拼接字符串"性能差?这和 StringBuffer 有什么关系?

每次 + 操作编译后约等于 new StringBuilder().append(s1).append(s2).toString()。在循环中反复执行意味着每次迭代都创建新的 StringBuilder 和临时 String 对象,造成大量 GC 压力。虽然编译器也做了优化(单条语句中连续 + 会被优化为一个 StringBuilder),但循环中的累积拼接无法被优化——必须手动使用 StringBuilder 或 StringBuffer。

Q4:StringBuffer 的 toString() 方法为什么也是 synchronized?

StringBuffer.toString() 需要读取内部的 char[] value 和 int count 字段来创建结果 String。如果不在同步块中,可能读到不一致的状态(例如 count 已被更新但 value 尚未写入完整)。加上 synchronized 保证读取时看到的是完整、一致的数据。


白歌在技术分享会上总结:"90% 的场景用 StringBuilder,9% 的场景用局部变量 + 线程封闭规避问题,只有不到 1% 的场景才真正需要 StringBuffer。知道什么时候用哪一个,就是程序员的专业素养。"

上一页
String:不可变的字符串
下一页
StringBuilder:可变的字符串构建器