
通过为共享资源 `stringbuilder` 添加同步机制(如 `synchronized` 块),并将其声明为 `final`,可保证多线程环境下日志写入与读取的原子性与可见性,避免数据竞争和内容错乱。
在多线程环境中,Logger 类当前实现存在严重的线程安全问题:多个线程并发调用 log() 方法时,会同时修改共享的 StringBuilder contents 对象,导致日志内容交错、时间戳或线程名错位,甚至引发 StringBuffer 内部状态不一致(尽管 StringBuilder 本身非线程安全,其方法也未做同步)。
最直接且可靠的修复方式是使用内置锁(intrinsic lock)对临界区进行同步。推荐以 contents 对象自身作为锁目标,并将其声明为 final——这不仅语义上表明该锁对象不可变,更关键的是确保锁的安全性与一致性:final 字段的初始化完成对所有线程可见,避免因指令重排序导致其他线程看到未完全构造的锁对象。
以下是改进后的线程安全版本:
public class Logger {
private final StringBuilder contents = new StringBuilder();
public void log(String message) {
synchronized (contents) {
contents.append(System.currentTimeMillis());
contents.append('[');
contents.append(Thread.currentThread().getName());
contents.append("]: ");
contents.append(message);
contents.append("\n");
}
}
public String getContents() {
synchronized (contents) {
return contents.toString();
}
}
}⚠️ 注意事项:
- 不要用 this 作锁:若外部代码也能获取该 Logger 实例并对其加锁,可能引发死锁或意外的锁竞争;
- 避免锁粒度过粗:本例中读写共用同一把锁,适合低并发或调试场景;高吞吐日志系统应考虑无锁方案(如 ConcurrentLinkedQueue 缓存日志条目 + 后台刷盘)或使用成熟的日志框架(Log4j2、SLF4J + Logback);
- StringBuilder vs StringBuffer:虽然 StringBuffer 是线程安全的,但其所有公共方法均加了 synchronized,性能开销大且无法满足自定义同步逻辑(如需保证“追加整条日志”为原子操作),因此仍推荐手动同步 StringBuilder;
- 内存可见性保障:synchronized 不仅互斥执行,还确保退出同步块前对 contents 的修改对后续进入该块的线程可见,符合 Java 内存模型(JMM)要求。
综上,合理使用 synchronized 块配合 final 锁对象,是在不引入额外依赖前提下实现轻量级线程安全日志收集的简洁、高效且易于理解的方案。










