一、前言

日期的转换与格式化在项目中应该是比较常用的了,最近同事小刚出去面试实在是没想到被 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. 使用Asponse.Words处理Word模板

    一.客户需求 近期接到一个项目,在与客户初步沟通后,客户描述的需求听起来也非常简单,就是目前客户需要在Excel录入数据,然后把这些数据分别复制到多个Word的多个地方,除了单个值之外,还需要复制表格 ...

  2. pytorch加载语音类自定义数据集

    pytorch对一下常用的公开数据集有很方便的API接口,但是当我们需要使用自己的数据集训练神经网络时,就需要自定义数据集,在pytorch中,提供了一些类,方便我们定义自己的数据集合 torch.u ...

  3. n阶行列式的全排列求解(Java)

    上一个随笔,我介绍了全排列的递归求解,其中还有排列的逆序数等代码,这次我来介绍如何使用全排列计算行列式的值. 使用全排列求行列式的值,简单的描述就是: 对这个行列式每一行选取一个数,这些数处于行列式的 ...

  4. Redux学习day1

    01.React介绍 Redux是一个用来管理管理数据状态和UI状态的JavaScript应用工具.随着JavaScript单页应用(SPA)开发日趋复杂,JavaScript需要管理比任何时候都要多 ...

  5. 正式班D25

    2020.11.09星期一 正式班D25 目录 13.7 LVM 13.7.1 lvm简介 13.7.2 lvm基本使用 13.7.3 在线动态扩容 13.7.4 在线动态缩容与删除 13.7.5 快 ...

  6. Prometheus安装配置

    一.什么是Prometheus? Prometheus受启发于Google的Brogmon监控系统(相似的Kubernetes是从Google的Brog系统演变而来),从2012年开始由前Google ...

  7. leetcode138container-with-water

    题目描述 给定n个非负整数a1,a2,-,an,其中每个数字表示坐标(i, ai)处的一个点.以(i,ai)和(i,0)(i=1,2,3...n)为端点画出n条直线.你可以从中选择两条线与x轴一起构成 ...

  8. codeforces 1425E,一万种情况的简单题

    大家好,欢迎阅读codeforces专题. 我们今天选中的是codeforces 1425场比赛的E题,这是一场印尼多校联合的ICPC的练习赛.ACM赛制,难度也比较近似.我们今天选择的是其中的一道M ...

  9. 国内申请苹果美区ID

    首先声明,纯粹是自己为了玩游戏而找到的方法..... 不用翻墙,在国内,就可以申请非国区的apple ID. 1.你自己申请一个自己的国区的apple ID,地址:https://appleid.ap ...

  10. php 获取抖音id

    <?php public function getid($dy_url){ $header=get_headers($dy_url); $str = "/^.*?(\d+).*/&qu ...