最近一直在忙于救火,陆陆续续的有不少用户反馈我们的系统会出错,下单时间不是在远古就是在未来。而负责后台管理系统的员工也提出了质疑,这个订单下单时间是 19xx 年,还有这个订单创建时间是 2187 年,这些都是非正常的订单,是不是有人攻击我们?这些订单我们是否要取消?
一连串的问题,微信工单群里都上千条消息了,不得已,大半夜的起来查看服务器日志。原来是 SimpleDateFormat 在捣鬼。
SimpleDateFormat 线程安全是一个老生常谈的问题了。为什么还发生在我们公司?为什么测试没测出来?等等这些问题,属于管理问题,我不在细说。今天我主要来复牌,内部培训,我也拿出这篇文章给大家看的。
什么是线程安全?
建议阅读我的这篇文章《Java 线程安全的3大核心:原子性、可见性、有序性》。
SimpleDateFormat 线程安全问题
在阿里巴巴的 Java 开发手册中,已经明确的写明了禁止使用 static 的 SimpleDateFormat,但是我们还有人用。
我从日志中看到了非常多的 java.lang.NumberFormatException: multiple points 异常信息。打印的日期有 0014-01-17,2214-01-17 等。定位到相关代码后,先让业务限制在夜间进行秒杀、活动推广等操作,直到第二天问题被修复,重新发版。
问题代码
为了讲清楚这个问题,我专门简化了业务逻辑,整理了关键点给各位同事和网友演示。
首先,我们有一个 DateUtil 的工具类,内容大致如下:
/** * DateUtil * @author xtt * @date 2019/1/17 下午1:14 */ public class DateUtil { private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); private static SimpleDateFormat timeSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static String xttblog(String timeString) throws ParseException { Date date = timeSdf.parse(timeString); return sdf.format(date); } }
然后在秒杀等活动下单时,创建订单有一个格式化时间操作,调用了 DateUtil 类。代码简化如下:
package com.xttblog.test; import java.util.concurrent.CountDownLatch; /** * SimpleDateFormatTest * @author xtt * @date 2019/1/17 下午1:14 */ public class SimpleDateFormatTest { static int threadCount = 20; final static CountDownLatch latch = new CountDownLatch(threadCount); public static void main(String[] args) { for (int i = 0;i < threadCount; i++){ new Thread(){ public void run() { try { System.out.println("子线程 " + Thread.currentThread().getName() + " 正在执行"); System.out.println(DateUtil.xttblog("2019-01-17 14:00:00")); System.out.println("子线程 " + Thread.currentThread().getName() + " 执行完毕"); latch.countDown(); } catch (Exception e) { e.printStackTrace(); } }; }.start(); } } }
这才相当于 20 个并发,并不高。实际上只有两个 2 线程并发时,也会偶尔发生线程安全问题。
为什么 SimpleDateFormat 不是线程安全的?
我们先来看一张类图:
再结合 SimpleDateFormat 类的源码,可知每个 SimpleDateFormat 实例里面有一个 Calendar 对象,SimpleDateFormat 之所以是线程不安全的就是因为 Calendar 是线程不安全的,后者之所以是线程不安全的是因为其中存放日期数据的变量都是线程不安全的,比如里面的 fields,time 等。
说白了,parse 并不是一个原子性的操作。当出现下面的情况时,就会发生线程安全问题。
解决方案
这类问题的解决方案有很多,大家按照阿里巴巴 java 开发规范中推荐的用法去用即可。我的案例代码,只需要稍作调整即可。
public class DateUtil { private static ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<SimpleDateFormat>(){ @Override protected SimpleDateFormat initialValue(){ return new SimpleDateFormat("yyyy-MM-dd"); } }; private static ThreadLocal<SimpleDateFormat> timeSdf = new ThreadLocal<SimpleDateFormat>(){ @Override protected SimpleDateFormat initialValue(){ return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } }; public static String xttblog(String timeString) throws ParseException { Date date = timeSdf.get().parse(timeString); return sdf.get().format(date); } }
运行效果如下:
你说这个问题难吗?一点也不难吧,难的是一些人不愿意去学习,必须要推着转,强迫他们。出了问题了,首先找别人,责任推的一干二净。
: » SimpleDateFormat 线程安全问题让我们穿越到远古和未来
原创文章,作者:端木书台,如若转载,请注明出处:https://blog.ytso.com/tech/java/252016.html