时间处理的坑:SimpleDateFormat与DateTimeFormatter

目录

问题描述

异常定位

排查路线

解决及思考

解决方案

思考

附-问题重现


问题描述

分享一个时间格式处理的坑。

昨天晚上的时候,同事遇到了一个问题,客户投诉说我们系统同步给他们的订单创建时间和他们系统保存的创建时间出现了大范围的不一致现象,影响到了别的相关业务系统业务,要求我们立刻核查处理(这里解释一下,订购方,我方,落地方系统需要定时对比订单信息,三方比对保证订单信息一致)

因为大晚上,比较无聊,我刚好和同事在聊天,所以一块看了看这个问题

异常定位

首先,出现这种问题,我们考虑了最有可能出现的问题

1、我们在接收订单的时候,对方传输的订单数据存在异常

2、我们接受订单,对于时间的转换存在问题

3、DAO框架不当的使用,加上数据库默认值配置的问题

4、其他业务场景对订单的更新造成的问题

排查路线

开始挨个排查可能的原因

1、查找当时的订购日志,确认订购方发给我们的订单数据是否存在异常

经排查,订购方发送的订购数据正常。

2、查看程序是否抛出异常或错误

过滤全链路监控日志,无相关异常或错误

3、查看代码与数据库,是否设置了统一时间或默认值

经查看,无特殊设置赋值,DBA告知无默认值设置

4、询问相关同事及负责人,其他业务对此订单库表的使用情况

确认完毕,无其他业务或系统使用此订单库表,此订单库表为当前服务独享

喂喂喂,怎么肥四,怎么都没问题,干脆说不找了吧这个问题,让他们自己看着办修复吧

解决及思考

开个玩笑,当然要解决

经过上面的排查,几乎可以定位,问题点不在数据库,不在外部系统,在于内部对时间的处理

重新研究关于订单数据时间上的处理

发现代码如下:

String recordTime = sf.format(orderDate);

sf为:

private SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

对方格式为 yyyyMMddhhmmss

考虑问题出在这儿的转换上

此刻我已经猜测到问题所在,因为SimpleDateFormat 线程不安全,并发场景下会存在数据问题

但出于严谨,和同事进行了测试

1、造出一部分数据

2、在测试环境进行并发访问

3、查看数据信息

测试完成,确认此处转换存在问题

解决方案

1、使用JDK8提供的线程安全的类 DateTimeFormatter 进行时间格式转换

2、进行格式转换前,对于 SimpleDateFormat ,不再公用,单独new,保证线程独享

思考

虽然问题已经解决,但是为什么出现这个问题,还是可以探索一下的

SimpleDateFormat.format() 不安全,为什么不安全?只能查看源码,看能否找到答案

注意,format点进来,默认的是dateformat这个抽象类

我们要找到下面的真正的实现方法, SimpleDateFormat.format() 方法

 可以看到,SimpleDateFormat.format() 调用了内部的 format 方法

 这就是 SimpleDateFormat.format() 对时间格式的处理,⚠️ 注意,整个处理,是没有加锁的,也就是说,多线程情况下,存在竞争问题

看我圈的这一行

假设A线程设置了一个时间,B线程同时设置了另一个时间,这个时候,日历时间就已经不再正确 

下边不管干啥,也没什么意义了

 所以说,考虑线程安全问题很重要

附-问题重现

附上问题重新代码及情况

我们只要模拟多个线程共用一个 SimpleDateFormat 就可以复现线程安全问题,为此,我准备了一份代码

   public static void main(String[] args) throws InterruptedException, ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/user?useUnicode=true&characterEncoding=UTF-8&useSSL=false&rewriteBatchedStatements=true", "root", "root");
        Statement statement = connection.createStatement();
        ResultSet resultSet = statement.executeQuery("select create_time from users");

        List<Date> list = new ArrayList<>();
        ResultSetMetaData metaData = resultSet.getMetaData();
        int columnCount = metaData.getColumnCount();
        while (resultSet.next()) {
            list.add(resultSet.getDate("create_time"));
        }

        SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
        ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
        CountDownLatch countDownLatch = new CountDownLatch(1);

        for (Date date : list) {
            new Thread(() -> {
                try {
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                map.put(new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(date), sf.format(date));
            }).start();
        }


        for (int i = 0; i < 100; i++) {

        }
        countDownLatch.countDown();

        Thread.sleep(10000L);

        for (String key : map.keySet()) {
            System.out.println(key + " " + map.get(key));
        }
    }

1、为了模拟场景真实与可靠,我创建了一张表,放入完全不同的创建时间,读取出这些时间

2、为了更加真实的模拟线程竞争的情况,一个时间创建一个线程,并阻塞这些线程

3、同步唤醒这些线程,尽管计数器底层也是挨个唤醒,但这点时间几乎可以忽略不计

3、为了更好的看出时间的区别,采用线程安全的map显示转换前后

4、key使用单独的独享的转换,value使用公共的转换

运行,打印,结果如下:

ok,本次的填坑记录到此为止,闪人,睡觉~

<think>好的,我现在要帮用户介绍一下SimpleDateFormat、String和Date之间的转换。首先,我需要确认用户对这三个类的了解程度。可能用户已经知道它们的基本用法,但想更深入地理解如何相互转换,或者在实际应用中需要注意的地方。 首先,我应该从基础开始,解释每个类的作用。Date类代表特定的时间点,而String是日期的文本表示。SimpleDateFormat则是用来格式化和解析日期的工具。接下来,用户关心的应该是如何在这三者之间转换,比如Date转String,String转Date,以及可能涉及的格式问题。 接下来,我需要考虑用户可能遇到的问题,比如日期格式的字符串模式,比如"yyyy-MM-dd HH:mm:ss",这里要提醒用户注意大小写,比如月份是大写的M,分钟是小写的m,避免混淆。另外,SimpleDateFormat不是线程安全的,这点在实际开发中很重要,尤其是在多线程环境下使用时需要特别注意,可能需要使用ThreadLocal或者每次创建新实例。 然后,用户可能会遇到解析异常的情况,比如输入的字符串格式不匹配,或者含有非法字符。这时候需要提到ParseException,并建议在代码中进行异常处理,给出示例代码中的try-catch块。 另外,时区的问题也是一个关键点。SimpleDateFormat默认使用系统的时区,如果在处理不同时区的日期时,可能需要显式设置时区,比如setTimeZone方法,避免出现时间偏差。 还有,Java 8之后的DateTimeFormatter可能更推荐使用,但用户可能还在维护旧代码,所以需要说明SimpleDateFormat仍然相关,但新项目建议使用新的API。 在结构上,应该分步骤讲解,比如先讲Date转String,再讲String转Date,然后注意事项。每个步骤给出代码示例,并解释关键点。比如在转换时,格式字符串必须匹配,否则会出错。 还要考虑用户可能的应用场景,比如日志处理、数据导入导出中的日期格式转换,这时候正确的格式化和解析就非常重要。 最后,总结一下整个转换流程,强调格式一致性和异常处理的重要性,确保用户在实际应用中能避免常见的。</think>在Java中,`SimpleDateFormat`、`String`和`Date`之间的转换主要用于日期字符串的相互格式化,以下是具体转换方法和注意事项: --- ### 一、核心类作用 - **`Date`**:表示特定时间点的类(位于`java.util.Date`) - **`String`**:日期的文本表示形式(如 `"2023-08-01 14:30:00"`) - **`SimpleDateFormat`**:用于定义日期格式的类(位于`java.text.SimpleDateFormat`) --- ### 二、转换流程 #### 1. Date → String ```java // 定义日期格式(例如:yyyy-MM-dd HH:mm:ss) SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 创建Date对象 Date now = new Date(); // 格式化为String String dateStr = sdf.format(now); // 输出结果示例:"2023-08-01 14:30:00" ``` #### 2. String → Date ```java // 定义字符串匹配的格式 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); // 待解析的字符串 String input = "2023-08-01"; // 解析为Date对象 try { Date date = sdf.parse(input); } catch (ParseException e) { e.printStackTrace(); // 处理格式不匹配的情况 } ``` --- ### 三、格式符号对照表 | 符号 | 含义 | 示例 | |------|---------------|------------| | y | 年 | yyyy → 2023 | | M | 月 | MM → 08 | | d | 日 | dd → 01 | | H | 小时(24小时制)| HH → 14 | | h | 小时(12小时制)| hh → 02 | | m | 分钟 | mm → 30 | | s | 秒 | ss → 45 | | S | 毫秒 | SSS → 500 | --- ### 四、关键注意事项 1. **线程安全问题** `SimpleDateFormat`是非线程安全的! ✅ 解决方案: - 每次使用时创建新实例 - 使用`ThreadLocal<SimpleDateFormat>` - 改用Java 8的`DateTimeFormatter`(推荐) 2. **格式严格匹配** 输入字符串必须格式完全匹配,否则会抛出`ParseException`: ```java // 错误示例:字符串多出空格 new SimpleDateFormat("yyyy-MM-dd").parse("2023-08-01 "); // 报错 ``` 3. **时区处理** 默认使用系统时区,可通过`setTimeZone()`修改: ```java SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); sdf.setTimeZone(TimeZone.getTimeZone("GMT+8")); // 设置为东八区 ``` 4. **月份分钟区分** - `M`表示月份 - `m`表示分钟 (常见错误:误用`MM`表示分钟) --- ### 五、Java 8+ 替代方案 推荐使用新的日期API(更安全、功能更丰富): ```java // 使用DateTimeFormatter LocalDateTime now = LocalDateTime.now(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String str = now.format(formatter); // Date → String LocalDateTime date = LocalDateTime.parse(str, formatter); // String → Date ``` --- ### 总结流程图 ``` Date对象 SimpleDateFormat String │ │ │ │ sdf.format(date) │ │ │─────────────────────────>│ │ │ │ │ │ │ sdf.parse(str) │ │<─────────────────────────│ │ │ │<─────────────────────────────│ ```
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值