转载至:ImportNew的《还在使用 SimpleDateFormat ?你的项目崩没?》

作者:周宇峰

突然看到ImportNew的相关推送,想起了以前踩过的坑,特此转载顺便记录一下。

先看看《阿里巴巴开发手册》对于SimpleDateFormat是怎么看待的:

问题场景复现

一般我们使用SimpleDateFormat的时候会把它定义为一个静态变量,避免频繁创建它的对象实例,如下代码:

public class SimpleDateFormatTest {private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public static String formatDate(Date date) throws ParseException {return sdf.format(date);}public static Date parse(String strDate) throws ParseException {return sdf.parse(strDate);}public static void main(String[] args) throws InterruptedException, ParseException {System.out.println(sdf.format(new Date()));}
}

是不是感觉没什么毛病?单线程下自然没毛病了,都是运用到多线程下就有大问题了。

测试下:

public static void main(String[] args) throws InterruptedException, ParseException {ExecutorService service = Executors.newFixedThreadPool(100);for (int i = 0; i < 20; i++) {service.execute(() -> {for (int j = 0; j < 10; j++) {try {System.out.println(parse("2018-01-02 09:45:59"));} catch (ParseException e) {e.printStackTrace();}}});}// 等待上述的线程执行完service.shutdown();service.awaitTermination(1, TimeUnit.DAYS);
}

控制台打印结果:

你看这不崩了?部分线程获取的时间不对,部分线程直接报java.lang.NumberFormatException: multiple points错,线程直接挂死了。

多线程不安全原因

因为我们吧SimpleDateFormat定义为静态变量,那么多线程下SimpleDateFormat的实例就会被多个线程共享,B线程会读取到A线程的时间,就会出现时间差异和其它各种问题。SimpleDateFormat和它继承的DateFormat类也不是线程安全的

来看看SimpleDateFormat的format()方法的源码

// Called from Format after creating a FieldDelegate
private StringBuffer format(Date date, StringBuffer toAppendTo,FieldDelegate delegate) {// Convert input date to time field listcalendar.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.setTime(date);,SimpleDateFormat的format方法实际操作的就是Calendar。

因为我们声明SimpleDateFormat为static变量,那么它的Calendar变量也就是一个共享变量,可以被多个线程访问。

假设线程A执行完calendar.setTime(date),把时间设置成2019-01-02,这时候被挂起,线程B获得CPU执行权。线程B也执行到了calendar.setTime(date),把时间设置为2019-01-03。线程挂起,线程A继续走,calendar还会被继续使用(subFormat方法),而这时calendar用的是线程B设置的值了,而这就是引发问题的根源,出现时间不对,线程挂死等等。

其实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.

意思就是

日期格式不同步。

建议为每个线程创建单独的格式实例。

如果多个线程同时访问一种格式,则必须在外部同步该格式。

解决方案

只在需要的时候创建新实例,不用static修饰

public static String formatDate(Date date) throws ParseException {SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");return sdf.format(date);
}public static Date parse(String strDate) throws ParseException {SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");return sdf.parse(strDate);
}

如上代码,仅在需要用到的地方创建一个新的实例,就没有线程安全问题,不过也加重了创建对象的负担,会频繁地创建和销毁对象,效率较低。

synchronized大法好

private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public static String formatDate(Date date) throws ParseException {synchronized(sdf){return sdf.format(date);}
}public static Date parse(String strDate) throws ParseException {synchronized(sdf){return sdf.parse(strDate);}
}

简单粗暴,synchronized往上一套也可以解决线程安全问题,缺点自然就是并发量大的时候会对性能有影响,线程阻塞。

ThreadLocal

private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {@Overrideprotected DateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");}
};public static Date parse(String dateStr) throws ParseException {return threadLocal.get().parse(dateStr);
}public static String format(Date date) {return threadLocal.get().format(date);
}

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

基于JDK1.8的DateTimeFormatter

也是《阿里巴巴开发手册》给我们的解决方案,对之前的代码进行改造:

public class SimpleDateFormatTest {private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");public static String formatDate2(LocalDateTime date) {return formatter.format(date);}public static LocalDateTime parse2(String dateNow) {return LocalDateTime.parse(dateNow, formatter);}public static void main(String[] args) throws InterruptedException, ParseException {ExecutorService service = Executors.newFixedThreadPool(100);// 20个线程for (int i = 0; i < 20; i++) {service.execute(() -> {for (int j = 0; j < 10; j++) {try {System.out.println(parse2(formatDate2(LocalDateTime.now())));} catch (Exception e) {e.printStackTrace();}}});}// 等待上述的线程执行完service.shutdown();service.awaitTermination(1, TimeUnit.DAYS);}
}

运行结果就不贴了,不会出现报错和时间不准确的问题。

DateTimeFormatter源码上作者也加注释说明了,他的类是不可变的,并且是线程安全的。

* This class is immutable and thread-safe.

ok,现在是不是可以对你项目里的日期工具类进行一波优化了呢?

SimpleDateFormat的相关学习踩坑记相关推荐

  1. 支付宝接口学习踩坑记(2)——基于SpringBoot的Alipay-EasySDK配置及网页支付接口简单示例

    Alipay-EasySDK从配置到撒钱 添加SDK依赖 Maven Gradle 配置SDK 准备材料 代码配置 编写代码,准备撒钱 后台逻辑代码 前端调用代码 撒钱开始 打开页面 电脑支付网页 P ...

  2. Vue + TypeScript + Element 搭建简洁时尚的博客网站及踩坑记

    前言 本文讲解如何在 Vue 项目中使用 TypeScript 来搭建并开发项目,并在此过程中踩过的坑 . TypeScript 具有类型系统,且是 JavaScript 的超集,TypeScript ...

  3. 口罩、安全帽识别比赛踩坑记(二) 比赛流程及 SSD / YOLO V3 两版本实现

    本篇文章主要对比赛流程中的各个环节进行展开说明,并对笔者践行过的代码及更改的地方进行记录.如哪里有侵权请联系笔者进行删除.另外在这里对比赛举办方表示感谢 ~ ~ 其中开源代码会在整理后放在github ...

  4. python从入门到实践django看不懂_Python编程:从入门到实践踩坑记 Django

    <>踩坑记 Django Django Python 19.1.1.5 模板new_topic 做完书上的步骤后,对主题添加页面经行测试,但是浏览器显示 服务器异常. 个人采用的开发环境是 ...

  5. 东八区转为0时区_踩坑记 | Flink 天级别窗口中存在的时区问题

    ❝ 本系列每篇文章都是从一些实际的 case 出发,分析一些生产环境中经常会遇到的问题,抛砖引玉,以帮助小伙伴们解决一些实际问题.本文介绍 Flink 时间以及时区问题,分析了在天级别的窗口时会遇到的 ...

  6. Spring @Transactional踩坑记

    @Transactional踩坑记 总述 ​ Spring在1.2引入@Transactional注解, 该注解的引入使得我们可以简单地通过在方法或者类上添加@Transactional注解,实现事务 ...

  7. Spark踩坑记——数据库(Hbase+Mysql)转

    转自:http://www.cnblogs.com/xlturing/p/spark.html 前言 在使用Spark Streaming的过程中对于计算产生结果的进行持久化时,我们往往需要操作数据库 ...

  8. android小程序_小程序踩坑记

    小程序踩坑记 希望这个文章能尽量记录下小程序的那些坑,避免开发者们浪费自己的生命来定位到底是自己代码导致的还是啥神秘的字节跳变原因. 前记 小程序大多数坑是同一套代码在不同平台上表现不一致导致的,微信 ...

  9. go nil json.marshal 完是null_字节跳动踩坑记#3:Go服务灵异panic

    这个坑比较新鲜,刚填完,还冒着冷气. - 1 - 在字节跳动,我们服务的所有 log 都通过统一的日志库采集到流式日志服务.落地 ES 集群,配上字节云超(sang)级(xin)强(bing)大(ku ...

最新文章

  1. 倒下的RoadStar结局来了:VC清盘止损,三位创业者承担1亿债务,打着官司各投林...
  2. C++ string类中的find()函数的简单使用
  3. 简述python函数调用过程_python函数定义和调用过程详解
  4. Spring Boot Jpa多数据源配置
  5. [luoguP1773] 符文之语_NOI导刊2010提高(02)(DP)
  6. python控制modem的at指令_硬件设计MCU架构+Python命名空间和作用域
  7. Java 面向对象的设计原则
  8. 软件开发工作者的重心是什么
  9. 【演化计算】【论文研读】Surrogate-Assisted Evolutionary DL Using E2E Random Forest-based Performance Predictor
  10. 郭慧计算机,机械制图及CAD(高等院校网络教育系列教材)
  11. 外媒点赞,浪潮存储为何能入围全球最佳主存储供应商
  12. CSAPP:第四章——处理器体系结构(上)
  13. nginx rua代码同步非阻塞
  14. php 访问服务器上图片不显示,php显示云服务器上图片不显示
  15. 多家支付机构叫停网络销售POS机 但“POSS机”“破死机”还在
  16. 第30届深圳礼品展暨1688工厂直采季开幕,携手创增长
  17. 基于Python的Open3D库模型数据简单读取测试
  18. 一篇文章带你学会 Spring 中的 JdbcTemplate 增删改查操作
  19. 张扬建:遥感与生态学的结合
  20. 搜狗技术开放日招募令 | 免费报名,与搜狗AI大咖面对面!

热门文章

  1. 0基础该如何快速入门python?全写在这篇学习攻略里了
  2. 利用js来制作简单倒计时效果
  3. 2020年9月23日总结
  4. 为什么选择预付费表? 安科瑞 戈静怡
  5. Word文字怎么弄成斜体的
  6. 如果把谷歌数据中心的数据都用打孔卡存起来
  7. 巧用UserAgent来解决浏览器的各种问题
  8. 系统学习机器学习之弱监督学习(二)--半监督学习综述
  9. 协作翻译平台 调研 version 0.5
  10. Python玩微信跳一跳教程