一、前言

日期的转换与格式化在项目中应该是比较常用的了,最近同事小刚出去面试实在是没想到被 SimpleDateFormat 给摆了一道...

面试官:项目中的日期转换怎么用的?SimpleDateFormat 用过吗?能说一下 SimpleDateFormat 线程安全问题吗,以及如何解决?

同事小刚:用过的,平时就是在全局定义一个 static 的 SimpleDateFormat,然后在业务处理方法(controller)中直接使用,至于线程安全... 这个... 倒是没遇到过线程安全问题。

哎,面试官的考察点真的是难以捉摸,吐槽归吐槽,一起来看看这个类吧。

二、概述

SimpleDateFormat 类主要负责日期的转换与格式化等操作,在多线程的环境中,使用此类容易造成数据转换及处理的不正确,因为 SimpleDateFormat 类并不是线程安全的,但在单线程环境下是没有问题的。

SimpleDateFormat 在类注释中也提醒大家不适用于多线程场景:

Date formats are not synchronized.
It is recommended to create separate format instances for each thread.
If multiple threads access a format concurrently, it must be synchronized
externally.

日期格式不同步。
建议为每个线程创建单独的格式实例。 
如果多个线程同时访问一种格式,则必须在外部同步该格式。

来看看阿里巴巴 java 开发规范是怎么描述 SimpleDateFormat 的:

三、模拟线程安全问题

无码无真相,接下来我们创建一个线程来模拟 SimpleDateFormat 线程安全问题:

创建 MyThread.java 类:

public class MyThread extends Thread{
  
    private SimpleDateFormat simpleDateFormat;
   /* 要转换的日期字符串 */
    private String dateString;

    public MyThread(SimpleDateFormat simpleDateFormat, String dateString){
        this.simpleDateFormat = simpleDateFormat;
        this.dateString = dateString;
    }

    @Override
    public void run() {
        try {
            Date date = simpleDateFormat.parse(dateString);
            String newDate = simpleDateFormat.format(date).toString();
            if(!newDate.equals(dateString)){
                System.out.println("ThreadName=" + this.getName()
                    + " 报错了,日期字符串:" + dateString
                    + " 转换成的日期为:" + newDate);
            }
        }catch (ParseException e){
            e.printStackTrace();
        }
    }
}

创建执行类 Test.java 类:

public class Test {

    // 一般我们使用SimpleDateFormat的时候会把它定义为一个静态变量,避免频繁创建它的对象实例
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("YYYY-MM-dd");

    public static void main(String[] args) {

        String[] dateStringArray = new String[] { "2020-09-10", "2020-09-11", "2020-09-12", "2020-09-13", "2020-09-14"};

        MyThread[] myThreads = new MyThread[5];

        // 创建线程
        for (int i = 0; i < 5; i++) {
            myThreads[i] = new MyThread(simpleDateFormat, dateStringArray[i]);
        }

        // 启动线程
        for (int i = 0; i < 5; i++) {
            myThreads[i].start();
        }
    }
}

执行截图如下:

从控制台打印的结果来看,使用单例的 SimpleDateFormat 类在多线程的环境中处理日期转换,极易出现转换异常(java.lang.NumberFormatException:multiple points)以及转换错误的情况。

四、线程不安全的原因

这个时候就需要看看源码了,format() 格式转换方法:

// 成员变量 Calendar
protected Calendar calendar;

private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {
    // Convert input date to time field list
    calendar.setTime(date);

    boolean useDateFormatSymbols = useDateFormatSymbols();

    for (int i = 0; i < compiledPattern.length; ) {
        int tag = compiledPattern[i] >>> 8;
        int count = compiledPattern[i++] & 0xff;
        if (count == 255) {
            count = compiledPattern[i++] << 16;
            count |= compiledPattern[i++];
        }

        switch (tag) {
        case TAG_QUOTE_ASCII_CHAR:
            toAppendTo.append((char)count);
            break;

        case TAG_QUOTE_CHARS:
            toAppendTo.append(compiledPattern, i, count);
            i += count;
            break;

        default:
            subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
            break;
        }
    }
    return toAppendTo;
}

我们把重点放在 calendar ,这个 format 方法在执行过程中,会操作成员变量 calendar 来保存时间 calendar.setTime(date)

但由于在声明 SimpleDateFormat 的时候,使用的是 static 定义的,那么这个 SimpleDateFormat 就是一个共享变量,SimpleDateFormat 中的 calendar 也就可以被多个线程访问到,所以问题就出现了,举个例子:

假设线程 A 刚执行完 calendar.setTime(date) 语句,把时间设置为 2020-09-01,但线程还没执行完,线程 B 又执行了 calendar.setTime(date) 语句,把时间设置为 2020-09-02,这个时候就出现幻读了,线程 A 继续执行下去的时候,拿到的 calendar.getTime 得到的时间就是线程B改过之后的。

除了 format() 方法以外,SimpleDateFormat 的 parse 方法也有同样的问题。

至此,我们发现了 SimpleDateFormat 的弊端,所以为了解决这个问题就是不要把 SimpleDateFormat 当做一个共享变量来使用。

五、如何解决线程安全

1、每次使用就创建一个新的 SimpleDateFormat

创建全局工具类 DateUtils.java

public class DateUtils {
    public static Date parse(String formatPattern, String dateString) throws ParseException {
        return new SimpleDateFormat(formatPattern).parse(dateString);
    }

    public static String  format(String formatPattern, Date date){
        return new SimpleDateFormat(formatPattern).format(date);
    }
}

所有用到 SimpleDateFormat 的地方全部用 DateUtils 替换,然后看一下执行结果:

好家伙,异常+错误终于是没了,这种解决处理错误的原理就是创建了多个 SimpleDateFormat 类的实例,在需要用到的地方创建一个新的实例,就没有线程安全问题,不过也加重了创建对象的负担,会频繁地创建和销毁对象,效率较低。

2、synchronized 锁

synchronized 就不展开介绍了,不了解的小伙伴请移步 > synchronized的底层原理?

变更一下 DateUtils.java

public class DateUtils {

    private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static Date parse(String formatPattern, String dateString) throws ParseException {
        synchronized (simpleDateFormat){
            return simpleDateFormat.parse(dateString);
        }
    }

    public static String format(String formatPattern, Date date) {
        synchronized (simpleDateFormat){
            return simpleDateFormat.format(date);
        }
    }
}

简单粗暴,synchronized 往上一套也可以解决线程安全问题,缺点自然就是并发量大的时候会对性能有影响,因为使用了 synchronized 加锁后的多线程就相当于串行,线程阻塞,执行效率低。

3、ThreadLocal(最佳MVP)

ThreadLocal 是 java 里一种特殊的变量,ThreadLocal 提供了线程本地的实例,它与普通变量的区别在于,每个使用该线程变量的线程都会初始化一个完全独立的实例副本。

继续改造 DateUtils.java

public class DateUtils {

    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    };

    public static Date parse(String formatPattern, String dateString) throws ParseException {
        return threadLocal.get().parse(dateString);
    }

    public static String format(String formatPattern, Date date) {
        return threadLocal.get().format(date);
    }
}

ThreadLocal 可以确保每个线程都可以得到单独的一个 SimpleDateFormat 的对象,那么就不会存在竞争问题。

如果项目中还在使用 SimpleDateFormat 的话,推荐这种写法,但这样就结束了吗?

显然不是...

六、项目中推荐的写法

上边提到的阿里巴巴 java 开发手册给出了说明:如果是 JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe。

日期转换,SimpleDateFormat 固然好用,但是现在我们已经有了更好地选择,Java 8 引入了新的日期时间 API,并引入了线程安全的日期类,一起来看看。

  • Instant:瞬时实例。
  • LocalDate:本地日期,不包含具体时间 例如:2014-01-14 可以用来记录生日、纪念日、加盟日等。
  • LocalTime:本地时间,不包含日期。
  • LocalDateTime:组合了日期和时间,但不包含时差和时区信息。
  • ZonedDateTime:最完整的日期时间,包含时区和相对UTC或格林威治的时差。

新API还引入了 ZoneOffSet 和 ZoneId 类,使得解决时区问题更为简便。

解析、格式化时间的 DateTimeFormatter 类也进行了全部重新设计。

例如,我们使用 LocalDate 代替 Date,使用 DateTimeFormatter 代替 SimpleDateFormat,如下所示:

// 当前日期和时间
String DateNow = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss")); 
System.out.println(DateNow);

这样就避免了 SimpleDateFormat 的线程不安全问题啦。

此时的 DateUtils.java

public class DateUtils {

    public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");

    public static LocalDate parse(String dateString){
        return LocalDate.parse(dateString, DATE_TIME_FORMATTER);
    }

    public static String format(LocalDate target) {
        return target.format(DATE_TIME_FORMATTER);
    }
}

七、最后总结

SimpleDateFormart 线程不安全问题

SimpleDateFormart 继承自 DateFormart,在 DataFormat 类内部有一个 Calendar 对象引用,SimpleDateFormat 转换日期都是靠这个 Calendar 对象来操作的,比如 parse(String),format(date) 等类似的方法,Calendar 在用的时候是直接使用的,而且是改变了 Calendar 的值,这样情况在多线程下就会出现线程安全问题,如果 SimpleDateFormart 是静态的话,那么多个 thread 之间就会共享这个 SimpleDateFormart,同时也会共享这个 Calendar 引用,那么就出现数据赋值覆盖情况,也就是线程安全问题。(现在项目中用到日期转换,都是使用的 java 8 中的 LocalDate,或者 LocalDateTime,本质是这些类是不可变类,不可变一定程度上保证了线程安全)。

解决方式

在多线程下可以使用 ThreadLocal 修饰 SimpleDateFormart,ThreadLocal 可以确保每个线程都可以得到单独的一个 SimpleDateFormat 的对象,那么就不会存在竞争问题。

项目中推荐的写法

java 8 中引入新的日期类 API,这些类是不可变的,且线程安全的。

以后面试官再问项目中怎么使用日期转换的,尽量就不要说 SimpleDateFormat 了。

博客园持续更新,欢迎关注,未来,我们一起成长。

本文首发于博客园:https://www.cnblogs.com/niceyoo/p/13672913.html

为什么SimpleDateFormat不是线程安全的?的更多相关文章

  1. 关于 SimpleDateFormat 的非线程安全问题及其解决方案

    一直以来都是直接用SimpleDateFormat开发的,没想着考虑线程安全的问题,特记录下来(摘抄的): 1.问题: 先来看一段可能引起错误的代码: package test.date; impor ...

  2. SimpleDateFormat一定是线程不安全吗?

    今天一位优秀的架构师告诉我,下面这段代码SimpleDateFormat是线程不安全的. /** * 将Date按格式转化成String * * @param date Date对象 * @param ...

  3. SimpleDateFormat类的线程安全问题和解决方案

    摘要:我们就一起看下在高并发下SimpleDateFormat类为何会出现安全问题,以及如何解决SimpleDateFormat类的安全问题. 本文分享自华为云社区<SimpleDateForm ...

  4. SimpleDateFormat使用和线程安全问题

    SimpleDateFormat 是一个以国别敏感的方式格式化和分析数据的具体类. 它允许格式化 (date -> text).语法分析 (text -> date)和标准化. Simpl ...

  5. sdf SimpleDateFormat 不是线程安全的,

    我经常用一个public static SimpleDateFormat sdf; 今天发现报“java.lang.NumberFormatException: multiple points”的异常 ...

  6. SimpleDateFormat 的性能和线程安全性

    系统正常运行一段时间后,QA报给我一个异常: java.lang.OutOfMemoryError: GC overhead limit exceeded at java.text.DecimalFo ...

  7. SimpleDateFormat 的线程安全问题与解决方式

    SimpleDateFormat 的线程安全问题 SimpleDateFormat 是一个以国别敏感的方式格式化和分析数据的详细类. 它同意格式化 (date -> text).语法分析 (te ...

  8. 转:浅谈SimpleDateFormat的线程安全问题

    转自:https://blog.csdn.net/weixin_38810239/article/details/79941964 在实际项目中,我们经常需要将日期在String和Date之间做转化, ...

  9. SimpleDateFormat线程不安全及解决的方法

    一. 为什么SimpleDateFormat不是线程安全的? Java源代码例如以下: /** * Date formats are not synchronized. * It is recomme ...

随机推荐

  1. JAVA学习线路:day14-网络编程

    心得: 我是一名正在自学的java的即将毕业的大学生 总结笔记是为了让自己更好的理解和加深印象.可能不是那么美观,自己可以看懂就好 所有的文档和源代码都开源在GitHub: https://githu ...

  2. 秒懂Dubbo接口(原理篇)

    引言 背景 单一应用架构 垂直应用架构 分布式服务架构 流动计算架构 为什么要用 Dubbo? 什么是分布式? 为什么要分布式? Dubbo 的架构 Dubbo 的架构图解 Dubbo 工作原理 Du ...

  3. 841. Keys and Rooms —— weekly contest 86

    题目链接:https://leetcode.com/problems/keys-and-rooms/description/ 简单DFS time:9ms 1 class Solution { 2 p ...

  4. swjtuoj2433 Magic Mirror

    描述 Magic Mirror is an artificial intelligence system developed by TAL AI LAB,It can determine human ...

  5. RabbitMQ相关概念的理解

    1.什么是消息? 消息就是程序(服务)之间传递的数据(图/文/声/像). 2.MQ是什么? MQ(MessageQueue)是指消息队列亦或消息总线.是消息的容器,这个容器的策略是FIFO(先进先出) ...

  6. Jenkins(9)- 汉化 Jenkins 网站

    如果想从头学起Jenkins的话,可以看看这一系列的文章哦 https://www.cnblogs.com/poloyy/category/1645399.html 下载插件 安装这两个插件,然后重启 ...

  7. Mac环境MySql初始密码设置

    1. 首先 点击系统偏好设置 -> 点击MySQL, 在弹出的页面中,关闭服务.2. 进入终端命令输出: cd /usr/local/mysql/bin/ 命令,回车.3. 回车后,输入命令:s ...

  8. 我画着图,FluentAPI 她自己就生成了

    在 Newbe.ObjectVistor 0.3 版本中我们非常兴奋的引入了一个紧张刺激的新特性:使用状态图来生成任意给定的 FluentAPI 设计. 开篇摘要 在非常多优秀的框架中都存在一部分 F ...

  9. 极客mysql08

    1.innodb支持RC和RR隔离级别实现是用的一致性视图(consistent read view) 2.事务在启动时会拍一个快照,这个快照是基于整个库的. 基于整个库的意思就是说一个事务内,整个库 ...

  10. C/C++宏替换详解

    目录 1. 基本形式 2. 宏展开中的陷阱 3. #undef 4. 宏参数.# 和 ## 1. 基本形式 #define name replacement_text 通常情况下,#define 指令 ...