本文目录导读:

- 目录导读
- 案例一:看似无辜的
HashMap——高并发下的死循环与数据丢失 - 案例二:
SimpleDateFormat的线程噩梦——日期格式化为何会“发疯”? - 案例三:
String.split()的”正则陷阱“——点号引发的血案 - 案例四:
finally块中的return——你以为的返回值真的是你以为吗? - 案例五:内存泄漏元凶——
ThreadLocal的错误使用 - 常见问题问答(FAQ)
- 总结与避坑建议
目录导读
- 看似无辜的
HashMap——高并发下的死循环与数据丢失 SimpleDateFormat的线程噩梦——日期格式化为何会“发疯”?String.split()的”正则陷阱“——点号引发的血案finally块中的return——你以为的返回值真的是你以为吗?- 内存泄漏元凶——
ThreadLocal的错误使用 - 常见问题问答(FAQ)
- 总结与避坑建议
看似无辜的HashMap——高并发下的死循环与数据丢失
场景还原
某金融系统在业务高峰时段频繁出现CPU 100%且线程卡死,堆栈日志显示线程全部阻塞在HashMap.put()方法上,开发人员反复审查代码逻辑,发现并未显式使用多线程操作该Map。
问题本质
在JDK 1.7及之前,HashMap在扩容(resize)时采用头插法,当多个线程同时触发扩容,会形成环形链表,后续get()操作遍历该链表时,将陷入无限循环,即使JDK 1.8改为了尾插法,HashMap的put操作在并发下仍可能丢失数据(两个线程同时写入同一个桶,后写覆盖先写)。
代码还原
// 错误演示:多线程共享一个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语句会覆盖try或catch块中的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,还有哪些常见类不是线程安全的?
答:ArrayList、LinkedList、HashSet、StringBuilder(注意是Builder,不是Buffer)、PriorityQueue等都是非线程安全的,其中StringBuilder在单线程下快于StringBuffer,但多线程下必须加锁或改用StringBuffer。
问:我用了ThreadLocal,但内存还是泄露,为什么?
答:可能性①忘记调用remove();可能性②使用了静态ThreadLocal,且线程池的线程复用,导致所有线程都持有同一个ThreadLocal值,建议在ThreadLocal的set方法调用后,在finally中调用remove(),或者在ThreadLocal值对象内部持有清理标记。
问:String.split()有哪些其他正则陷阱?
答:(竖线)在正则中表示“或”,需要写成;表示行尾,需要写成;和都是量词,需要写成和,建议优先使用Pattern.quote()方法将用户输入转义。
总结与避坑建议
| 疑难症状 | 根本原因 | 解决方案 |
|---|---|---|
| 高并发Map死循环 | 非线程安全实现 | 使用ConcurrentHashMap |
| 日期解析错乱 | SDF内部状态非线程安全 | 使用DateTimeFormatter或ThreadLocal |
| Split结果空 | 正则元字符未转义 | 使用或StringUtils |
| finally返回错误值 | finally中return覆盖 | 禁止在finally中return |
| 老年代溢出 | ThreadLocal未被清理 | 使用后必须remove |
最后的核心建议:
- 写并发代码前,先明确你的集合类和工具类是否线程安全。
- 使用任何静态工具类时,查看JDK文档中的“线程安全”说明。
- 养成“用完即清理”的习惯,尤其是
ThreadLocal、IO流、数据库连接。 - 多使用Java 8+的时间API(
java.time),彻底远离SimpleDateFormat。
本文所涉及代码均为简化示例,实际生产中请结合具体业务场景进行完整测试,Java的坑还有很多,但掌握这些高频问题,能让你的线上稳定性大幅提升。