日常开发中,我们经常需要使用时间相关类,说到时间相关类,想必大家对SimpleDateFormat并不陌生。主要是用它进行时间的格式化输出和解析,挺方便快捷的,但是SimpleDateFormat并不是一个线程安全的类。在多线程情况下,会出现异常,想必有经验的小伙伴也遇到过。下面我们就来分析分析SimpleDateFormat为什么不安全?是怎么引发的?以及多线程下有那些SimpleDateFormat的解决方案?

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

附《阿里巴巴Java开发手册》v1.4.0(详尽版)下载链接:https://yfzhou.oss-cn-beijing.aliyuncs.com/blog/img/《阿里巴巴开发手册》v 1.4.0.pdf

问题场景复现

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

copypublic 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()));}}

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

copypublic 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()方法的源码

copy// Called from Format after creating a FieldDelegateprivate 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源码上作者也给过我们提示:

copy* 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修饰

copypublic 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大法好

copyprivate 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

copyprivate 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

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

copypublic 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源码上作者也加注释说明了,他的类是不可变的,并且是线程安全的。

copy* This class is immutable and thread-safe.

SimpleDateFormat的坑相关推荐

  1. SimpleDateFormat小坑

    先上代码: String dateStr = "2021-10-29";SimpleDateFormat dateFormat = new SimpleDateFormat(&qu ...

  2. 查询两个日期间隔天数怎么算_大厂都是怎么用Java8代替SimpleDateFormat?

    1 SimpleDateFormat 之坑 1.1 格式化 1.1.1 案例 初始化一个Calendar,设置日期2020年12月29日 ​ 日志 ​ 这是由于混淆SimpleDateFormat的各 ...

  3. simpledateformat格式_大厂都是怎么用Java8代替SimpleDateFormat?

    点击上方"JavaEdge",关注公众号 设为"星标",好文章不错过! 1 SimpleDateFormat 之坑 1.1 格式化 1.1.1 案例 初始化一个 ...

  4. 线上故障之-数据库问题

    线上故障之-数据库问题 数据库问题概述 索引: 高可用 一些需要注意的事项 处理问题的一些技巧 一般大厂数据库规约 一.基础规范 二.命名规范 三.表设计规范 四.字段设计规范 五.索引设计规范 死锁 ...

  5. 【优雅的避坑】不安全!别再共享SimpleDateFormat变量了

    0x01 开场白 JDK文档中已经明确表明了SimpleDateFormat不应该用在多线程场景中: Synchronization Date formats are not synchronized ...

  6. SimpleDateFormat的相关学习踩坑记

    转载至:ImportNew的<还在使用 SimpleDateFormat ?你的项目崩没?> 作者:周宇峰 突然看到ImportNew的相关推送,想起了以前踩过的坑,特此转载顺便记录一下. ...

  7. java yyyy m d_JAVA SimpleDateFormat使用YYYY-MM-dd的坑

    YYYY和yyyy的区别: 例子:Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.YEAR, 2019); cale ...

  8. Java日常开发的21个坑,你踩过几个?

    前言 最近看了极客时间的<Java业务开发常见错误100例>,再结合平时踩的一些代码坑,写写总结,希望对大家有帮助,感谢阅读~ 1. 六类典型空指针问题 包装类型的空指针问题 级联调用的空 ...

  9. 这些Java8官方挖的坑,你踩过几个?

    导读:系统启动异常日志竟然被JDK吞噬无法定位?同样的加密方法,竟然出现部分数据解密失败?往List里面添加数据竟然提示不支持?日期明明间隔1年却输出1天,难不成这是天上人间?1582年神秘消失的10 ...

最新文章

  1. 详解zabbix中文版安装部署
  2. TensorFlow入门
  3. 蓝桥杯 历届试题 合根植物(并查集)
  4. qstring去掉特定字符_如何花式、批量且操作简单地处理字符?
  5. Visual C++中最常用的类与API函数
  6. (81)什么是原型验证?
  7. android-activity生命周期方法
  8. (三)、一步一步学GTK+之布局
  9. [转载] python实现三角形面积计算
  10. 使用WireMock 伪造 Rest 服务
  11. JDK9API网盘下载
  12. php 验证手机号邮箱,PHP使用正在表达检查是否未手机号码或者邮箱
  13. mysql崩 数据同步_MySQL5.7 大大降低了半同步复制-数据丢失的风险
  14. 多个路由器相连接的方式(以及配置成交换机的方式)
  15. macOS High Sierra 10.13.6(17G65) IWith Clover 4596 and winPE含N显卡驱动
  16. Linux扩容swap分区
  17. 显示12306服务器处理中正在排队,12306排队等待中怎么办 12306一直在排队解决方法(图文)...
  18. 简析国内外电商的区别
  19. BlueTooth: 浅析CC2540的OSAL原理
  20. 解决中标麒麟QQ乱码和WPS缺失字体的错误

热门文章

  1. linux手动安装rsync_在Linux/Unix上安装rsync并通过示例的方式介绍使用rsync命令
  2. 关于Linux下C语言编程execvp函数的一个问题
  3. sse——字符串数组
  4. accumulate详细用法
  5. MySQL数据库编程01
  6. idea连接oracle可插拔数据库报ORA-12505
  7. C/C++ fstream
  8. 鼠标事件(mouseover和mouseenter)
  9. Linux权限委派(生产环境必备)
  10. Linux环境下安装部署redis