在Java里线程不安全的常见场景有哪些_Java并发问题说明

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

共享变量未加锁直接读写

多个线程同时读写同一个 intString 或自定义对象字段,且没用 synchronizedvolatile 或原子类,就会出现值被覆盖、丢失更新。比如两个线程都执行 counter++(本质是读-改-写三步),很可能最终只加了 1 次而不是 2 次。

常见于计数器、状态标志、缓存计数等场景。注意:volatile 能保证可见性和禁止重排序,但不能保证复合操作的原子性——volatile int countcount++ 仍是线程不安全的。

  • AtomicInteger 替代普通 int 做计数
  • 临界区代码块必须用 synchronized(this) 或显式 ReentrantLock
  • 避免在 getter/setter 中直接暴露可变对象引用(如返回内部 ArrayList 实例)

非线程安全集合被并发修改

ArrayListHashMapHashSet 这些类本身不保证线程安全。多线程往里面 add()put(),轻则数据丢失,重则触发 ConcurrentModificationException,甚至死循环(JDK 7 中 HashMap 扩容时链表成环)。

不是所有“正在遍历中修改”才出错——即使只是多个线程纯写入,也存在结构不一致风险。

  • 优先用 CopyOnWriteArrayList(适合读多写少)、ConcurrentHashMap(高并发读写首选)
  • 避免用 Collections.synchronizedList(new ArrayList()) 后忘记对迭代操作额外加锁
  • ConcurrentHashMapsize()isEmpty() 返回近似值,不能用于条件判断逻辑

静态工具类或单例持有可变状态

看似“无状态”的工具类,如果内部缓存了 SimpleDateFormatRandom 或某个 StringBuilder 实例并复用,就极易出问题。

例如 SimpleDateFormatparse()format() 方法不是线程安全的,共享一个实例会导致解析错乱、格式化结果异常。

这种问题隐蔽性强,单元测试往往跑不出,压测或线上流量突增时才暴露。

  • 每次使用都新建 SimpleDateFormat(注意对象创建开销)
  • 改用 DateTimeFormatter(JDK 8+,不可变、线程安全)
  • 若必须复用,用 ThreadLocal 隔离实例
  • 检查所有 static 字段:是否无意中成了多线程共享的可变状态容器

错误地认为“局部变量天然线程安全”而忽略逃逸

局部变量本身确实在线程栈上,但一旦它引用的对象被发布到其他线程可见的作用域(比如作为参数传给另一个线程、放进共享队列、赋值给静态字段),那它的状态就不再受线程栈保护。

典型例子:在方法里 new 出一个 ArrayList,往里塞数据后 add 到全局 Queue;或者把局部 StringBuilder 传给异步回调函数——这些对象的内容可能正被多个线程同时修改。

  • 向共享结构传递对象前,考虑是否需要深拷贝或不可变封装(如 ImmutableList.copyOf(list)
  • 避免在 lambda 表达式或匿名内部类中直接捕获可变局部变量并跨线程使用
  • @NotThreadSafe@ThreadSafe 注解明确标注类的设计意图,辅助团队识别风险点

真正危险的不是“不知道要加锁”,而是“以为自己已经锁住了”,比如锁对象选错(用了局部变量当锁)、同步块范围太小、或误信某些 JDK 类的线程安全性(像 TreeMap 就不是线程安全的,哪怕 ConcurrentHashMap 是)。多线程调试没法靠单步,得靠设计时的明确边界和防御性编码习惯。

猜你喜欢

联络方式:

400 9058 355

邮箱:8955556@qq.com

Q Q:8955556

微信二维码
在线咨询 拨打电话

电话

400 9058 355

微信二维码

微信二维码