Java疑难杂症案例

wen java案例 1

本文目录导读:

Java疑难杂症案例

  1. 目录导读
  2. 案例一:看似无辜的HashMap——高并发下的死循环与数据丢失
  3. 案例二:SimpleDateFormat的线程噩梦——日期格式化为何会“发疯”?
  4. 案例三:String.split()的”正则陷阱“——点号引发的血案
  5. 案例四:finally块中的return——你以为的返回值真的是你以为吗?
  6. 案例五:内存泄漏元凶——ThreadLocal的错误使用
  7. 常见问题问答(FAQ)
  8. 总结与避坑建议

目录导读

  1. 看似无辜的HashMap——高并发下的死循环与数据丢失
  2. SimpleDateFormat的线程噩梦——日期格式化为何会“发疯”?
  3. String.split()的”正则陷阱“——点号引发的血案
  4. finally块中的return——你以为的返回值真的是你以为吗?
  5. 内存泄漏元凶——ThreadLocal的错误使用
  6. 常见问题问答(FAQ)
  7. 总结与避坑建议

看似无辜的HashMap——高并发下的死循环与数据丢失

场景还原

某金融系统在业务高峰时段频繁出现CPU 100%且线程卡死,堆栈日志显示线程全部阻塞在HashMap.put()方法上,开发人员反复审查代码逻辑,发现并未显式使用多线程操作该Map。

问题本质

在JDK 1.7及之前,HashMap在扩容(resize)时采用头插法,当多个线程同时触发扩容,会形成环形链表,后续get()操作遍历该链表时,将陷入无限循环,即使JDK 1.8改为了尾插法,HashMapput操作在并发下仍可能丢失数据(两个线程同时写入同一个桶,后写覆盖先写)。

代码还原

// 错误演示:多线程共享一个HashMap
Map<String, String> map = new HashMap<>();
// 线程A和线程B同时执行 map.put("key", value)

解决方案

  • 使用ConcurrentHashMap:其分段锁(JDK 1.7)或CAS+synchronized(JDK 1.8)机制,保证线程安全。
  • 使用Collections.synchronizedMap():虽安全,但锁粒度大,性能较差。
  • 避免在并发场景使用HashMap,即使只有一个线程在写,多个线程在读,也建议使用ConcurrentHashMap

SimpleDateFormat的线程噩梦——日期格式化为何会“发疯”?

场景还原

一个定时任务每天凌晨解析大量日期字符串,运行一段时间后抛出NumberFormatException或解析出完全错误的时间(如2023年解析成2025年),错误发生时,代码中并未修改任何日期变量。

问题本质

SimpleDateFormat内部维护一个Calendar对象用于日期计算,该对象不是线程安全的,当多个线程并发调用parse()format()时,Calendar的字段会被多个线程同时修改和读取,导致数据错乱。

代码表现

private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// 多线程调用 sdf.parse("2023-01-01") 导致异常

解决方案

  • 使用ThreadLocal为每个线程维护独立的SimpleDateFormat实例。
  • 使用Java 8的DateTimeFormatter,它是不可变且线程安全的。
  • 避免使用全局静态SimpleDateFormat
private static final ThreadLocal<SimpleDateFormat> tl = 
    ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

String.split()的”正则陷阱“——点号引发的血案

场景还原

开发人员解析IP地址"192.168.1.1",使用str.split("."),结果返回的是一个空数组,线上日志流出异常,导致系统无法识别IP。

问题本质

String.split(String regex)参数是正则表达式,而在正则中表示“匹配任意字符”,所以"192.168.1.1".split(".")实际上是按每个字符切割,且由于正则匹配到的位置是每个字符之间,最终会得到一个空数组(具体行为取决于JDK版本,但结果都不是预期的)。

解决方案

  • 转义:str.split("\\.")
  • 使用StringTokenizer(但官方不推荐)。
  • 使用Apache Commons的StringUtils.split(str, '.')(注意这是普通字符分隔)。

finally块中的return——你以为的返回值真的是你以为吗?

场景还原

一段代码中,在try块里会返回一个计算结果,但在异常时必须返回默认值,开发者在finally块中写了return defaultValue;,结果无论是否异常,方法总是返回默认值,正确的计算结果被吞掉了。

问题本质

finally块中的return语句会覆盖trycatch块中的return语句,如果finally中包含return,JVM会忽略前者的返回值,直接使用finally中的值。finally块中如果抛异常,也会覆盖原有异常。

代码表现

public int getValue() {
    try {
        return 1;
    } finally {
        return 2; // 最终返回2
    }
}

避坑指南

  • 永远不要在finally块中使用return
  • 如果需要在异常时返回默认值,请在catch块中处理。
  • finally必须清理资源,确保不吞掉业务异常(可使用try-with-resources)。

内存泄漏元凶——ThreadLocal的错误使用

场景还原

某Web应用运行两周后,老年代内存持续增长,最终抛出OutOfMemoryError: Java heap space,通过MAT分析发现,大量ThreadLocal对象的Entry引用仍然存活着,且其值对象是一个巨大的业务缓存。

问题本质

ThreadLocal的实现是基于ThreadLocalMap,其Entry继承自WeakReference(弱引用,键被GC回收),但值(Value)是强引用,如果线程生命周期很长(如线程池中的线程),且使用完毕后没有调用remove(),那么即使ThreadLocal的Key被回收,Value依然被线程的ThreadLocalMap持有,导致内存无法被回收。

代码还原

private static ThreadLocal<MyCache> cache = new ThreadLocal<>();
// 在请求方法中设置
cache.set(new MyCache());
// 但忘记调用 cache.remove();

解决方案

  • 每次使用后必须调用remove(),尤其是线程池环境。
  • 建议在finally块中执行remove()
  • 非必要不使用ThreadLocal,如果可以用参数传递,尽量避免。

常见问题问答(FAQ)

问:ConcurrentHashMap能完全替代HashMap吗? 答:不能。ConcurrentHashMap的读操作通常不加锁(JDK 1.8采用volatile和CAS),但其迭代器是弱一致的,即迭代时无法反映最新修改,且ConcurrentHashMap不允许null键和null值(因为并发环境下无法确定null值是否表示“不存在”),若业务需要null,只能使用HashMap加外部锁。

问:除了SimpleDateFormat,还有哪些常见类不是线程安全的? 答:ArrayListLinkedListHashSetStringBuilder(注意是Builder,不是Buffer)、PriorityQueue等都是非线程安全的,其中StringBuilder在单线程下快于StringBuffer,但多线程下必须加锁或改用StringBuffer

问:我用了ThreadLocal,但内存还是泄露,为什么? 答:可能性①忘记调用remove();可能性②使用了静态ThreadLocal,且线程池的线程复用,导致所有线程都持有同一个ThreadLocal值,建议在ThreadLocalset方法调用后,在finally中调用remove(),或者在ThreadLocal值对象内部持有清理标记。

问:String.split()有哪些其他正则陷阱? 答:(竖线)在正则中表示“或”,需要写成;表示行尾,需要写成;和都是量词,需要写成和,建议优先使用Pattern.quote()方法将用户输入转义。


总结与避坑建议

疑难症状 根本原因 解决方案
高并发Map死循环 非线程安全实现 使用ConcurrentHashMap
日期解析错乱 SDF内部状态非线程安全 使用DateTimeFormatter或ThreadLocal
Split结果空 正则元字符未转义 使用或StringUtils
finally返回错误值 finally中return覆盖 禁止在finally中return
老年代溢出 ThreadLocal未被清理 使用后必须remove

最后的核心建议:

  • 写并发代码前,先明确你的集合类和工具类是否线程安全。
  • 使用任何静态工具类时,查看JDK文档中的“线程安全”说明。
  • 养成“用完即清理”的习惯,尤其是ThreadLocalIO流数据库连接
  • 多使用Java 8+的时间API(java.time),彻底远离SimpleDateFormat

本文所涉及代码均为简化示例,实际生产中请结合具体业务场景进行完整测试,Java的坑还有很多,但掌握这些高频问题,能让你的线上稳定性大幅提升。

抱歉,评论功能暂时关闭!