SimpleDateFormat 线程安全问题让我们穿越到远古和未来

JAVA herman 789浏览
公告:“业余草”微信公众号提供免费CSDN下载服务(只下Java资源),关注业余草微信公众号,添加作者微信:xttblog,发送下载链接帮助你免费下载!
本博客日IP超过1800,PV 2600 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog,之前的微信号好友位已满,备注:返现
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
视频教程免费领

最近一直在忙于救火,陆陆续续的有不少用户反馈我们的系统会出错,下单时间不是在远古就是在未来。而负责后台管理系统的员工也提出了质疑,这个订单下单时间是 19xx 年,还有这个订单创建时间是 2187 年,这些都是非正常的订单,是不是有人攻击我们?这些订单我们是否要取消?

一连串的问题,微信工单群里都上千条消息了,不得已,大半夜的起来查看服务器日志。原来是 SimpleDateFormat 在捣鬼。

SimpleDateFormat 线程安全问题

SimpleDateFormat 线程安全是一个老生常谈的问题了。为什么还发生在我们公司?为什么测试没测出来?等等这些问题,属于管理问题,我不在细说。今天我主要来复牌,内部培训,我也拿出这篇文章给大家看的。

什么是线程安全?

建议阅读我的这篇文章《Java 线程安全的3大核心:原子性、可见性、有序性》。

SimpleDateFormat 线程安全问题

在阿里巴巴的 Java 开发手册中,已经明确的写明了禁止使用 static 的 SimpleDateFormat,但是我们还有人用。

阿里巴巴java开发手册 SimpleDateFormat

我从日志中看到了非常多的 java.lang.NumberFormatException: multiple points 异常信息。打印的日期有 0014-01-17,2214-01-17 等。定位到相关代码后,先让业务限制在夜间进行秒杀、活动推广等操作,直到第二天问题被修复,重新发版。

SimpleDateFormat 非线程安全

问题代码

为了讲清楚这个问题,我专门简化了业务逻辑,整理了关键点给各位同事和网友演示。

首先,我们有一个 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 类的源码,可知每个 SimpleDateFormat 实例里面有一个 Calendar 对象,SimpleDateFormat 之所以是线程不安全的就是因为 Calendar 是线程不安全的,后者之所以是线程不安全的是因为其中存放日期数据的变量都是线程不安全的,比如里面的 fields,time 等。

说白了,parse 并不是一个原子性的操作。当出现下面的情况时,就会发生线程安全问题。

Calendar 线程不安全

解决方案

这类问题的解决方案有很多,大家按照阿里巴巴 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);
    }
}

运行效果如下:

ThreadLocal

你说这个问题难吗?一点也不难吧,难的是一些人不愿意去学习,必须要推着转,强迫他们。出了问题了,首先找别人,责任推的一干二净。

业余草公众号

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加QQ1群:135430763(2000人群已满),QQ2群:454796847(已满),QQ3群:187424846(已满)。QQ群进群密码:xttblog,想加微信群的朋友,之前的微信号好友已满,请加博主新的微信号:xttblog,备注:“xttblog”,添加博主微信拉你进群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作可添加助理微信进行沟通!

本文原文出处:业余草: » SimpleDateFormat 线程安全问题让我们穿越到远古和未来