线程不安全问题主要表现为共享变量未加锁导致值覆盖、非线程安全集合并发修改异常、工具类复用引发状态错乱、局部变量逃逸破坏线程隔离,需用原子类、并发集合、ThreadLocal、不可变对象及正确同步机制防范。

共享变量未加锁直接读写
多个线程同时读写同一个 int、String 或自定义对象字段,且没用 synchronized、volatile 或原子类,就会出现值被覆盖、丢失更新。比如两个线程都执行 counter++(本质是读-改-写三步),很可能最终只加了 1 次而不是 2 次。
常见于计数器、状态标志、缓存计数等场景。注意:volatile 能保证可见性和禁止重排序,但不能保证复合操作的原子性——volatile int count 的 count++ 仍是线程不安全的。
- 用
AtomicInteger替代普通int做计数 - 临界区代码块必须用
synchronized(this)或显式ReentrantLock - 避免在 getter/setter 中直接暴露可变对象引用(如返回内部
ArrayList实例)
非线程安全集合被并发修改
ArrayList、HashMap、HashSet 这些类本身不保证线程安全。多线程往里面 add() 或 put(),轻则数据丢失,重则触发 ConcurrentModificationException,甚至死循环(JDK 7 中 HashMap 扩容时链表成环)。
不是所有“正在遍历中修改”才出错——即使只是多个线程纯写入,也存在结构不一致风险。
立即学习“Java免费学习笔记(深入)”;
- 优先用
CopyOnWriteArrayList(适合读多写少)、ConcurrentHashMap(高并发读写首选) - 避免用
Collections.synchronizedList(new ArrayList())后忘记对迭代操作额外加锁 -
ConcurrentHashMap的size()和isEmpty()返回近似值,不能用于条件判断逻辑
静态工具类或单例持有可变状态
看似“无状态”的工具类,如果内部缓存了 SimpleDateFormat、Random 或某个 StringBuilder 实例并复用,就极易出问题。例如 SimpleDateFormat 的 parse() 和 format() 方法不是线程安全的,共享一个实例会导致解析错乱、格式化结果异常。
这种问题隐蔽性强,单元测试往往跑不出,压测或线上流量突增时才暴露。
- 每次使用都新建
SimpleDateFormat(注意对象创建开销) - 改用
DateTimeFormatter(JDK 8+,不可变、线程安全) - 若必须复用,用
ThreadLocal隔离实例 - 检查所有
static字段:是否无意中成了多线程共享的可变状态容器
错误地认为“局部变量天然线程安全”而忽略逃逸
局部变量本身确实在线程栈上,但一旦它引用的对象被发布到其他线程可见的作用域(比如作为参数传给另一个线程、放进共享队列、赋值给静态字段),那它的状态就不再受线程栈保护。
典型例子:在方法里 new 出一个 ArrayList,往里塞数据后 add 到全局 Queue;或者把局部 StringBuilder 传给异步回调函数——这些对象的内容可能正被多个线程同时修改。
- 向共享结构传递对象前,考虑是否需要深拷贝或不可变封装(如
ImmutableList.copyOf(list)) - 避免在 lambda 表达式或匿名内部类中直接捕获可变局部变量并跨线程使用
- 用
@NotThreadSafe或@ThreadSafe注解明确标注类的设计意图,辅助团队识别风险点
真正危险的不是“不知道要加锁”,而是“以为自己已经锁住了”,比如锁对象选错(用了局部变量当锁)、同步块范围太小、或误信某些 JDK 类的线程安全性(像 TreeMap 就不是线程安全的,哪怕 ConcurrentHashMap 是)。多线程调试没法靠单步,得靠设计时的明确边界和防御性编码习惯。









