(转)关于SimpleDateFormat安全的时间格式化线程安全问题

想必大家对SimpleDateFormat并不陌生。SimpleDateFormat 是 Java 中一个非常常用的类,该类用来对日期字符串进行解析和格式化输出,但如果使用不小心会导致非常微妙和难以调试的问题,因为 DateFormat 和 SimpleDateFormat 类不都是线程安全的,在多线程环境下调用 format() 和 parse() 方法应该使用同步代码来避免问题。下面我们通过一个具体的场景来一步步的深入学习和理解SimpleDateFormat类。

  一.引子
  我们都是优秀的程序员,我们都知道在程序中我们应当尽量少的创建SimpleDateFormat 实例,因为创建这么一个实例需要耗费很大的代价。在一个读取数据库数据导出到excel文件的例子当中,每次处理一个时间信息的时候,就需要创建一个SimpleDateFormat实例对象,然后再丢弃这个对象。大量的对象就这样被创建出来,占用大量的内存和 jvm空间。代码如下:

复制代码
package com.peidasoft.dateformat;import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;public class DateUtil {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);}
}
复制代码

  你也许会说,OK,那我就创建一个静态的simpleDateFormat实例,然后放到一个DateUtil类(如下)中,在使用时直接使用这个实例进行操作,这样问题就解决了。改进后的代码如下:

复制代码
package com.peidasoft.dateformat;import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;public class DateUtil {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);}
}
复制代码

  当然,这个方法的确很不错,在大部分的时间里面都会工作得很好。但当你在生产环境中使用一段时间之后,你就会发现这么一个事实:它不是线程安全的。在正常的测试情况之下,都没有问题,但一旦在生产环境中一定负载情况下时,这个问题就出来了。他会出现各种不同的情况,比如转化的时间不正确,比如报错,比如线程被挂死等等。我们看下面的测试用例,那事实说话:

复制代码
package com.peidasoft.dateformat;import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;public class DateUtil {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);}
}
复制代码
复制代码
package com.peidasoft.dateformat;import java.text.ParseException;
import java.util.Date;public class DateUtilTest {public static class TestSimpleDateFormatThreadSafe extends Thread {@Overridepublic void run() {while(true) {try {this.join(2000);} catch (InterruptedException e1) {e1.printStackTrace();}try {System.out.println(this.getName()+":"+DateUtil.parse("2013-05-24 06:02:20"));} catch (ParseException e) {e.printStackTrace();}}}    }public static void main(String[] args) {for(int i = 0; i < 3; i++){new TestSimpleDateFormatThreadSafe().start();}}
}
复制代码

  执行输出如下:

复制代码
Exception in thread "Thread-1" java.lang.NumberFormatException: multiple pointsat sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1082)at java.lang.Double.parseDouble(Double.java:510)at java.text.DigitList.getDouble(DigitList.java:151)at java.text.DecimalFormat.parse(DecimalFormat.java:1302)at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311)at java.text.DateFormat.parse(DateFormat.java:335)at com.peidasoft.orm.dateformat.DateNoStaticUtil.parse(DateNoStaticUtil.java:17)at com.peidasoft.orm.dateformat.DateUtilTest$TestSimpleDateFormatThreadSafe.run(DateUtilTest.java:20)
Exception in thread "Thread-0" java.lang.NumberFormatException: multiple pointsat sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1082)at java.lang.Double.parseDouble(Double.java:510)at java.text.DigitList.getDouble(DigitList.java:151)at java.text.DecimalFormat.parse(DecimalFormat.java:1302)at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311)at java.text.DateFormat.parse(DateFormat.java:335)at com.peidasoft.orm.dateformat.DateNoStaticUtil.parse(DateNoStaticUtil.java:17)at com.peidasoft.orm.dateformat.DateUtilTest$TestSimpleDateFormatThreadSafe.run(DateUtilTest.java:20)
Thread-2:Mon May 24 06:02:20 CST 2021
Thread-2:Fri May 24 06:02:20 CST 2013
Thread-2:Fri May 24 06:02:20 CST 2013
Thread-2:Fri May 24 06:02:20 CST 2013
复制代码

  说明:Thread-1和Thread-0报java.lang.NumberFormatException: multiple points错误,直接挂死,没起来;Thread-2 虽然没有挂死,但输出的时间是有错误的,比如我们输入的时间是:2013-05-24 06:02:20 ,当会输出:Mon May 24 06:02:20 CST 2021 这样的灵异事件。

  二.原因

  作为一个专业程序员,我们当然都知道,相比于共享一个变量的开销要比每次创建一个新变量要小很多。上面的优化过的静态的SimpleDateFormat版,之所在并发情况下回出现各种灵异错误,是因为SimpleDateFormat和DateFormat类不是线程安全的。我们之所以忽视线程安全的问题,是因为从SimpleDateFormat和DateFormat类提供给我们的接口上来看,实在让人看不出它与线程安全有何相干。只是在JDK文档的最下面有如下说明:

  SimpleDateFormat中的日期格式不是同步的。推荐(建议)为每个线程创建独立的格式实例。如果多个线程同时访问一个格式,则它必须保持外部同步。

  JDK原始文档如下:
  Synchronization:
  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.

  下面我们通过看JDK源码来看看为什么SimpleDateFormat和DateFormat类不是线程安全的真正原因:

  SimpleDateFormat继承了DateFormat,在DateFormat中定义了一个protected属性的 Calendar类的对象:calendar。只是因为Calendar累的概念复杂,牵扯到时区与本地化等等,Jdk的实现中使用了成员变量来传递参数,这就造成在多线程的时候会出现错误。

  在format方法里,有这样一段代码:

复制代码
 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)这条语句改变了calendar,稍后,calendar还会用到(在subFormat方法里),而这就是引发问题的根源。想象一下,在一个多线程环境下,有两个线程持有了同一个SimpleDateFormat的实例,分别调用format方法:
  线程1调用format方法,改变了calendar这个字段。
  中断来了。
  线程2开始执行,它也改变了calendar。
  又中断了。
  线程1回来了,此时,calendar已然不是它所设的值,而是走上了线程2设计的道路。如果多个线程同时争抢calendar对象,则会出现各种问题,时间不对,线程挂死等等。
  分析一下format的实现,我们不难发现,用到成员变量calendar,唯一的好处,就是在调用subFormat时,少了一个参数,却带来了这许多的问题。其实,只要在这里用一个局部变量,一路传递下去,所有问题都将迎刃而解。
  这个问题背后隐藏着一个更为重要的问题--无状态:无状态方法的好处之一,就是它在各种环境下,都可以安全的调用。衡量一个方法是否是有状态的,就看它是否改动了其它的东西,比如全局变量,比如实例的字段。format方法在运行过程中改动了SimpleDateFormat的calendar字段,所以,它是有状态的。

  这也同时提醒我们在开发和设计系统的时候注意下一下三点:

  1.自己写公用类的时候,要对多线程调用情况下的后果在注释里进行明确说明

  2.对线程环境下,对每一个共享的可变变量都要注意其线程安全性

  3.我们的类和方法在做设计的时候,要尽量设计成无状态的

  三.解决办法

  1.需要的时候创建新实例:

复制代码
package com.peidasoft.dateformat;import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;public class DateUtil {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);}
}
复制代码

  说明:在需要用到SimpleDateFormat 的地方新建一个实例,不管什么时候,将有线程安全问题的对象由共享变为局部私有都能避免多线程问题,不过也加重了创建对象的负担。在一般情况下,这样其实对性能影响比不是很明显的。

  2.使用同步:同步SimpleDateFormat对象

复制代码
package com.peidasoft.dateformat;import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;public class DateSyncUtil {private static 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);}} 
}
复制代码

  说明:当线程较多时,当一个线程调用该方法时,其他想要调用此方法的线程就要block,多线程并发量大的时候会对性能有一定的影响。

  3.使用ThreadLocal: 

复制代码
package com.peidasoft.dateformat;import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;public class ConcurrentDateUtil {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);}
}
复制代码

  另外一种写法:

复制代码
package com.peidasoft.dateformat;import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;public class ThreadLocalDateUtil {private static final String date_format = "yyyy-MM-dd HH:mm:ss";private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(); 
public static DateFormat getDateFormat() { DateFormat df = threadLocal.get(); if(df==null){ df = new SimpleDateFormat(date_format); threadLocal.set(df); } return df; } public static String formatDate(Date date) throws ParseException {return getDateFormat().format(date);}public static Date parse(String strDate) throws ParseException {return getDateFormat().parse(strDate);} }
复制代码

  说明:使用ThreadLocal, 也是将共享变量变为独享,线程独享肯定能比方法独享在并发环境中能减少不少创建对象的开销。如果对性能要求比较高的情况下,一般推荐使用这种方法。

  4.抛弃JDK,使用其他类库中的时间格式化类:

  1.使用Apache commons 里的FastDateFormat,宣称是既快又线程安全的SimpleDateFormat, 可惜它只能对日期进行format, 不能对日期串进行解析。

  2.使用Joda-Time类库来处理时间相关问题

   

  做一个简单的压力测试,方法一最慢,方法三最快,但是就算是最慢的方法一性能也不差,一般系统方法一和方法二就可以满足,所以说在这个点很难成为你系统的瓶颈所在。从简单的角度来说,建议使用方法一或者方法二,如果在必要的时候,追求那么一点性能提升的话,可以考虑用方法三,用ThreadLocal做缓存。

  Joda-Time类库对时间处理方式比较完美,建议使用。

  参考资料:

  1.http://dreamhead.blogbus.com/logs/215637834.html

  2.http://www.blogjava.net/killme2008/archive/2011/07/10/354062.html

 

出处:http://www.cnblogs.com/peida/archive/2013/05/31/3070790.html

转载于:https://www.cnblogs.com/yuechuan/p/8981307.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/278063.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

IDEA开发工具的学习

1.设置jdk的版本 &#xff0c;快捷键&#xff1a;ctrl shirt alt s 打开项目的设置&#xff0c;选择Project 进行 jdk版本的设置。 2.鼠标移到项目上&#xff0c;右键&#xff0c;Show in Explorer 定位到当前项目对应的文件夹中 3.每次关闭项目时&#xff0c;需要手动选择Fi…

顺利达成微软HacktoberFest 2018

昨天收到邮件&#xff0c;我的HacktoberFest 2018奖品终于从美国寄出来了&#xff0c;不知道飘洋过海多久可以寄到。 今年的HacktoberFest 2018除了微软官方博客的宣传&#xff0c;连Channel 9的美女主播也在TWC上大肆宣传。 活动内容是在整个10月份需要给微软的开源代码贡献5…

更新!在线状态和用户的共存模式保持一致

根据用户反馈&#xff0c;我们正在改进&#xff1a;当组织同时使用Microsoft Teams和Skype for Business时的用户在线状态。通过此更新&#xff0c;路由和在线状态将完全保持一致。为确保路由能跟随用户的在线状态&#xff0c;所以在线状态的更新现在会基于用户的共存模式。 如…

centos上安装supervisor来管理dotnetcore等应用程序

supervisor 介绍&#xff1a;这是一款用python编写的进程管理工具&#xff0c;可以守护他管理的所有进程&#xff0c;防止异常退出&#xff0c;以及提供一个可视化的web界面来手动管理&#xff0c;打开关闭重启各种应用&#xff0c;界面如下&#xff1a;关于在centos上安装supe…

新增功能!Trello个人应用程序登陆 Microsoft Teams

从初创企业到《财富》500强公司, Trello是团队在任何项目上进行合作的视觉方式。在Microsoft Teams中, 我们发现围绕项目进行大量对话和协作的方式。因此, 一个首屈一指的项目管理工具应该与团队协作的终极枢纽进行合作, 以便让员工更好地一起工作。 如你所知, 我们已经为Micr…

Linux bc 命令简单学习

1. bash里面能够实现比较简单的四则运算 echo $((10*20)) 注意是 双括号 $ 地址符号. 2. 但是比较复杂的 可能就难以为继了 比如不支持精度 3. 所以这里面需要使用 bc 命令来执行相关的操作. man 内容: usage: bc [options] [file ...] -h --help print this usage and exit…

终于收到HacktoberFest的奖品啦

去年10月份给微软repo提交了5个PR&#xff0c;达成了HacktoberFest 2018&#xff0c;今天终于收到了从美国到澳洲&#xff0c;飘洋过海&#xff0c;姗姗来迟的T-shirt&#xff0c;不过大小正好。算是新年礼物了&#xff0c;哈哈

三、SpringBoot-application.properties配置文件和application.yml配置文件

其实SpringBoot的配置文件有.properties和.yml两种形式&#xff0c;两种配置文件的效果类似&#xff0c;只不过是格式不同而已&#xff0c;孩儿们可以根据下面这几种张截图&#xff0c;通过对比端口号的配置&#xff0c;以及连接SQLServer数据库的配置的书写格式来自己体会两者…

Teams中的快捷键让沟通协作更加高效

使用Teams的快捷键可以帮助我们提高日常沟通协作的效率。 一、快捷键分类 1.常规2.导航3.聊天输入界面4.团队和会议 1.常规 功能桌面版本快捷键WebApp版本快捷键搜索CtrlECtrlE显示命令CtrlSlash (/)CtrlSlash (/)gotoCtrlGCtrlShiftG开始新聊天CtrlNAltN打开设置CtrlComma …

线程间的协作(2)——生产者与消费者模式

2019独角兽企业重金招聘Python工程师标准>>> 1.何为生产者与消费者 在线程世界里&#xff0c;生产者就是生产数据的线程&#xff0c;消费者就是消费数据的线程。 import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.…

一位面试者提到直接调用vuex中mutations方法

简述是用this.$store.mutations.xxx(xx)方式调用&#xff0c;因从未见过此种调用方式&#xff0c;回来就搜索了一下&#xff0c;查询结果如下 首先前文&#xff1a; 获取 state 的方式有两种&#xff0c;分别是 this.$store.state.num 这种直接获取的方式&#xff0c;以及通过 …

从无到有到完善 - Teams抽奖机器人开发历程

我没有写博客有2&#xff0c;3个月了&#xff0c;好几个朋友来问我怎么不继续了。实际上这几个月我受到微软好友的鼓舞和鼓励&#xff0c;再加上今年2月1日有幸成为了微软中国区第一位Teams的MVP&#xff0c;所以决定不再停留于技术demo&#xff0c;而是使用微软最新的技术开发…

残差网络

作用&#xff1a;使得深层网络可以获得更好的性能&#xff0c;没有它&#xff0c;即使加深网络的层数无法直接获得性能的提升。 我的理解&#xff1a;1、使得低层的特征表示可以越层传递。 2、在反向传播时LOSS可以直接训练低层特征。 3、浅层网络的恒等映射&#xff0c;深层网…

Teams的MessageExtension最新功能:Initiate actions

官方文档到目前为止对这个initiate action的说明比较简洁&#xff0c;由于没有一步步的截图和说明&#xff0c;从头到尾看一遍可能还在云里雾里。 我一步步摸索着走了一遍&#xff0c;发现这个initiate action的功能如此强大&#xff0c;不敢独享&#xff0c;所以写此博文&…

Teams App抽奖机器人 - 基础架构

今天我们来聊一下&#xff0c;一个Teams app的infrastructure&#xff0c;我在考虑LuckyDraw的主要出于这么几个出发点&#xff1a; 可管理性。因为这是一个个人产品&#xff0c;以后维护工作也只有我一个人&#xff0c;所以我希望整个infrastructure简单、易管理&#xff0c;不…

如何做Teams Bot的测试覆盖

在我昨天的文章中介绍了如果对Teams bot做service level的测试&#xff0c;那到底要写多少的测试代码才算够&#xff1f;如何才算测试到位了&#xff1f;这个时候我们就需要用”测试覆盖率”来衡量&#xff0c;虽然覆盖率高并不一定代表着就可以高枕无忧的以为我们软件质量高了…

Spring Boot开发MongoDB应用实践

本文继续上一篇定时任务中提到的邮件服务&#xff0c;简单讲解Spring Boot中如何使用MongoDB进行应用开发。 上文中提到的这个简易邮件系统大致设计思路如下&#xff1a; 1、发送邮件支持同步和异步发送两种 2、邮件使用MongDB进行持久化保存 3、异步发送&#xff0c;直接将邮件…

QuickBI助你成为分析师-邮件定时推送

创建报表过程中经常需要将报表情况定时推送给其他用户&#xff0c;及时了解数据情况。高级版本邮件推送功能支持仪表板周期性推送到订阅人&#xff0c;默认以当前登录者视角查看&#xff0c;同时支持结合 行级权限进行权限控制 和 结合全局参数功能确定邮件推送内容参数&#x…

2019年5月 Teams Community Call (China)

这个月有四个话题&#xff1a; Tony Xia&#xff1a;这个月的Teams的产品更新&#xff0c;Teams开发能力的更新&#xff0c;开源项目更新&#xff0c;库更新王远&#xff1a;升级/迁移到Microsoft Teams刘钰&#xff1a;Teams账号注册探索指南Paul Zhang/Cheung&#xff1a;Bu…

在2019年6月Teams Community Call上分享的Teams app基础架构视频

我在2019年6月Teams Community Call(China)上分享的如何在azure上搭建典型的teams bot的基础架构 会议视频&#xff1a; 15:00 - 33:00 Download Video