点击上方小伟后端笔记关注公众号

每天阅读Java干货文章

一、前言

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

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

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

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

二、概述

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 synchronizedexternally.

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

来看看阿里巴巴 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             myThreads[i] = new MyThread(simpleDateFormat, dateStringArray[i]);        }

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

执行截图如下:


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

四、线程不安全的原因

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

// 成员变量 Calendarprotected 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         int tag = compiledPattern[i] >>> 8;        int count = compiledPattern[i++] & 0xff;        if (count == 255) {            count = compiledPattern[i++] <            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 threadLocal = new ThreadLocal(){        @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 了~

—END—精彩推荐因为BitMap,白白搭进去8台服务器...面试再被问到 ConcurrentHashMap,把这篇...8使用 EasyPOI 优雅导出Excel模板数据(含图片)字节一面,被连问 MySQL 索引,脸都问绿了

concurrenthashmap为什么是线程安全_为什么SimpleDateFormat不是线程安全的?相关推荐

  1. python判断线程结束_判断Threading.start新线程是否执行完毕的实例

    新写自己的Threading类 class MyThread(threading.Thread):#我的Thread类 判断流程结束没 用于os shell命令是否执行判断 def __init__( ...

  2. mongodb线程池_常用高并发网络线程模型设计及MongoDB线程模型优化实践

    服务端通常需要支持高并发业务访问,如何设计优秀的服务端网络IO工作线程/进程模型对业务的高并发访问需求起着至关重要的核心作用. 本文总结了了不同场景下的多种网络IO线程/进程模型,并给出了各种模型的优 ...

  3. std string与线程安全_详解linux系统中断线程的那些事

    很多情况下,使用信号来终止一个长时间运行的线程是合理的.这种线程的存在,可能是因为工作线程所在的线程池被销毁,或是用户显式的取消了这个任务,亦或其他各种原因.不管是什么原因,原理都一样:需要使用信号来 ...

  4. springboot tomcat默认线程数_记一次JAVA线程池的错误用法

    最近项目一个项目要结项了,但客户要求 TPS 能达到上千,而用我写的代码再怎么弄成只能达到 30 + 的 TPS,然后我又将代码中能缓存的都缓存了,能拆分的也都拆分了,拆分时用的线程池来实现的:其实现 ...

  5. java 线程状态_浅析Java中的线程状态

    一.线程的5种状态 众所周知,Java的线程状态有5种,分别对应上图中五种不同颜色,下面对这5种状态及状态间的转化做相应的解释: 1. 初始化状态:新建一个线程对象 2. 可运行状态:其他线程调用了该 ...

  6. java 线程等待_代码分析Java中线程的等待与唤醒

    我们先来看一下实例代码: class ThreadA extends Thread{ public ThreadA(String name) { super(name); } public void ...

  7. java解决线程死锁_为你解决Java线程死锁

    产生死锁的原因: 1. 系统资源不足.分配不当.系统中都会有一种不可剥夺的资源,若是这些资源不能够满足进程运行的需要,那么就只能进行资源争夺,从而陷入死锁. 注意:只有对不可剥夺资源的竞争才可能产生死 ...

  8. i5四核八线程怎么样_同样四核八线程,Ryzen 3 3100和3300X区别大了!

    AMD在21号晚上发布了Ryzen 3 3100和Ryzen 3 3300X两款主流级处理器,正好对位英特尔即将在月底发布的Core i3-10100和Core i3-10300.除了频率差别之外,这 ...

  9. 自定义java线程池_我的Java自定义线程池执行器

    自定义java线程池 ThreadPoolExecutor是Java并发api添加的一项功能,可以有效地维护和重用线程,因此我们的程序不必担心创建和销毁线程,也不必关注核心功能. 我创建了一个自定义线 ...

最新文章

  1. Django基础知识
  2. php根据IP地址跳转对应的城市,淘宝REST api调用地址直接使用
  3. Java自动部署maven_Maven+Tomcat8 实现自动化部署的方法
  4. swagger api文档_带有Swagger的Spring Rest API –创建文档
  5. 延长汽车寿命的6个良好习惯
  6. VTM3.0代码阅读:xCheckRDCostMerge2Nx2N函数
  7. 《思维训练500题》
  8. 能不用事务就尽量别用
  9. 简单人物画像_简易人物画像图
  10. Gradle学习笔记(二)
  11. 使用Bind提供域名解析服务
  12. ROM、RAM、DRAM、SRAM、FLASH区别
  13. 运放脉冲宽度放大_创鑫激光纳秒级脉冲激光器应用于精细焊接
  14. [导入]剿杀diskman.exe木马病毒
  15. 【牛客挑战赛63】圣遗物
  16. 【MySQL】字符集utf8mb4无法存储表情踩坑记录
  17. HDU - 相遇周期
  18. AI杂谈:从洗衣机到老鼠屁股
  19. thinkphp6 think-swoole websocket
  20. java模拟器带键盘安卓,如何使用android模拟器键盘的关键事件?

热门文章

  1. 如何用Python破解验证码,适合新手练手
  2. 一文搞懂Python知识难点------装饰器
  3. scrapy —— ImagePipeline
  4. 【已解决】清除linux系统的多余引导
  5. 每天进步一点点《ML - 线性回归》
  6. Python简介及环境搭建
  7. leetcode —— 面试题 04.03. 特定深度节点链表
  8. 吴恩达深度学习 —— 3.10 直观理解反向传播
  9. Correlated Topic model 的Gibbs sampling
  10. Java如何将指定字符串转化为指定日期格式