点击蓝色“程序猿DD”关注我哟

加个“星标”,不忘签到哦

来源:http://t.cn/EiYqmll


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

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

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

问题场景复现

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

  1. copypublic class SimpleDateFormatTest {

  2. private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

  3. public static String formatDate(Date date) throws ParseException {

  4. return sdf.format(date);

  5. }

  6. public static Date parse(String strDate) throws ParseException {

  7. return sdf.parse(strDate);

  8. }

  9. public static void main(String[] args) throws InterruptedException, ParseException {

  10. System.out.println(sdf.format(new Date()));

  11. }

  12. }

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

  1. copypublic static void main(String[] args) throws InterruptedException, ParseException {

  2. ExecutorService service = Executors.newFixedThreadPool(100);

  3. for (int i = 0; i < 20; i++) {

  4. service.execute(() -> {

  5. for (int j = 0; j < 10; j++) {

  6. try {

  7. System.out.println(parse("2018-01-02 09:45:59"));

  8. } catch (ParseException e) {

  9. e.printStackTrace();

  10. }

  11. }

  12. });

  13. }

  14. // 等待上述的线程执行完

  15. service.shutdown();

  16. service.awaitTermination(1, TimeUnit.DAYS);

  17. }

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

多线程不安全原因

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

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

  1. copy// Called from Format after creating a FieldDelegate

  2. private StringBuffer format(Date date, StringBuffer toAppendTo,

  3. FieldDelegate delegate) {

  4. // Convert input date to time field list

  5. calendar.setTime(date);

  6. boolean useDateFormatSymbols = useDateFormatSymbols();

  7. for (int i = 0; i < compiledPattern.length; ) {

  8. int tag = compiledPattern[i] >>> 8;

  9. int count = compiledPattern[i++] & 0xff;

  10. if (count == 255) {

  11. count = compiledPattern[i++] << 16;

  12. count |= compiledPattern[i++];

  13. }

  14. switch (tag) {

  15. case TAG_QUOTE_ASCII_CHAR:

  16. toAppendTo.append((char)count);

  17. break;

  18. case TAG_QUOTE_CHARS:

  19. toAppendTo.append(compiledPattern, i, count);

  20. i += count;

  21. break;

  22. default:

  23. subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);

  24. break;

  25. }

  26. }

  27. return toAppendTo;

  28. }

注意 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源码上作者也给过我们提示:

  1. copy* Date formats are not synchronized.

  2. * It is recommended to create separate format instances for each thread.

  3. * If multiple threads access a format concurrently, it must be synchronized

  4. * externally.

意思就是

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

解决方案

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

  1. copy

  2. public static String formatDate(Date date) throws ParseException {

  3. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

  4. return sdf.format(date);

  5. }

  6. public static Date parse(String strDate) throws ParseException {

  7. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

  8. return sdf.parse(strDate);

  9. }

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

synchronized大法好

  1. copyprivate static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

  2. public static String formatDate(Date date) throws ParseException {

  3. synchronized(sdf){

  4. return sdf.format(date);

  5. }

  6. }

  7. public static Date parse(String strDate) throws ParseException {

  8. synchronized(sdf){

  9. return sdf.parse(strDate);

  10. }

  11. }

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

ThreadLocal

  1. copyprivate static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {

  2. @Override

  3. protected DateFormat initialValue() {

  4. return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

  5. }

  6. };

  7. public static Date parse(String dateStr) throws ParseException {

  8. return threadLocal.get().parse(dateStr);

  9. }

  10. public static String format(Date date) {

  11. return threadLocal.get().format(date);

  12. }

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

基于JDK1.8的DateTimeFormatter

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

  1. copypublic class SimpleDateFormatTest {

  2. private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

  3. public static String formatDate2(LocalDateTime date) {

  4. return formatter.format(date);

  5. }

  6. public static LocalDateTime parse2(String dateNow) {

  7. return LocalDateTime.parse(dateNow, formatter);

  8. }

  9. public static void main(String[] args) throws InterruptedException, ParseException {

  10. ExecutorService service = Executors.newFixedThreadPool(100);

  11. // 20个线程

  12. for (int i = 0; i < 20; i++) {

  13. service.execute(() -> {

  14. for (int j = 0; j < 10; j++) {

  15. try {

  16. System.out.println(parse2(formatDate2(LocalDateTime.now())));

  17. } catch (Exception e) {

  18. e.printStackTrace();

  19. }

  20. }

  21. });

  22. }

  23. // 等待上述的线程执行完

  24. service.shutdown();

  25. service.awaitTermination(1, TimeUnit.DAYS);

  26. }

  27. }

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

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

  1. copy* This class is immutable and thread-safe.

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


推荐阅读

  • Nacos部署中的几个常见问题

  • 永远不要在 MySQL 中使用“utf8”

  • 大规模使用 Apache Kafka 的20个最佳实践

  • 永远不要在代码中使用「User」这个单词

  • 复仇者联盟与IntelliJ IDEA也很配哦

号外:最近整理了之前编写的一系列内容做成了PDF,关注我并回复相应口令获取:

001 :领取《Spring Boot基础教程》

002 :领取《Spring Cloud基础教程》


活动介绍:自律到极致-人生才精致:第5期

活动奖励:《微服务架构与实践(第2版)》* 10  

扫描下面二维码签到参与

关注我,加个星标,不忘签到哦~

2019

与大家聊聊技术人的斜杠生活

还在使用 SimpleDateFormat?你的项目崩没?相关推荐

  1. java 时间格式化_还在用SimpleDateFormat格式化时间?小心经理锤你

    还在用SimpleDateFormat格式化时间?小心经理锤你 场景 本来开开心心的周末时光,线上突然就疯狂报错,以为程序炸了,截停日志,发现是就是类似下述一段错误 java.lang.NumberF ...

  2. 【报错笔记】在做struts项目时,所有项目代码没问题但就是报404错误。

    在做struts项目时,所有项目代码没问题但就是报404错误,而且tomcat一次只能运行一个项目,做第二个得关闭tomcat,清空再开启运行. 我重新下载了tomcat9还是那样,可能是eclips ...

  3. 你还在从零搭建 Java 项目 ?10000 字总结~

    前言 在我的工作中,我从零开始搭建了不少软件项目,其中包含了基础代码框架和持续集成基础设施等,这些内容在敏捷开发中通常被称为"第0个迭代"要做的事情.但是,当项目运行了一段时间之后 ...

  4. 项目创建venv、_都2020年了,居然还有人没有在数据科学项目中使用Docker?

    本文将告诉大家什么是 Docker 以及如何在 Python 中使用 Docker. 新手程序员面临的最困难的问题之一是理解"环境"的概念.环境就是你编写代码的系统,听起来很容易, ...

  5. 日期格式化为yyyymmdd_你还在用SimpleDateFormat格式化时间嘛

    Jdk1.8之时间处理 该文章已经同步到Github:https://github.com/stackInk/makerstack 1. 传统时间处理的问题 1.1 多线程环境下的SimpleDate ...

  6. 代理服务器地址在哪里看_看完这篇还不了解Nginx,那我就没你办法了

    文章来自:php自学中心 链接:http://www.startphp.cn/front/skill/0211141.html 作者:yang 商务合作:请加微信(QQ):2230304070 视频教 ...

  7. java项目合同制没做完扣钱吗_程序员接私活,怎样防止做完不给钱?

    首先跟大家说明一点,我们做 IT 类的外包开发,是非标品开发,所以很有可能在开发过程中会有这样那样的需求修改,而这种需求修改很容易造成扯皮,进而影响到费用支付,甚至出现做完了项目收不到钱的情况. 那么 ...

  8. 面试被问项目上线没_从面试官角度谈观察到的程序员技能瓶颈

    Hi-新朋友,记得点 蓝字 关注我们哟 我在做技术面试官的时候,面试对象大多数是初级开发和高级开发,偶尔也会面试架构师.我发现,IT人的工资固然和相关技术的使用年限有关,但如果候选人的技能无法跨越到更 ...

  9. 【2022 CCF BDCI 文心大模型创意项目】没见过雪的我们

    一· 前言 呜呜呜有多少南方的孩子还没见过雪呢?反正我是没见过. 真的好想看看诗中"千里冰封,万里雪飘""忽如一夜春风来,千树万树梨花开""大雪压青松 ...

最新文章

  1. java程序设计实验报告_JavA程序设计实验报告.doc
  2. 学习笔记:CentOS 7学习之十一:文件的重定向
  3. QT自定义控件(生成和使用)
  4. Netty 源码走读
  5. 云南计算机专升本数据结构_怎么查找云南省2019年专升本计算机专业试题
  6. LeetCode OJ - Populating Next Right Pointers in Each Node II
  7. HDU 3669 Cross the Wall(斜率DP+预处理)
  8. python self理解_Python列表理解
  9. Java集合面试题(总结最全面的面试题)
  10. qq三国2015最新服务器,合服服务器查询
  11. Markdown记录
  12. Matlab 读取.grd文件
  13. 无线渗透----kismet工具使用
  14. 用计算机排序excel,【用excel名字排序的方法有哪些?这些简便的计算机技能你一定需要】- 环球网校...
  15. 【2】Kali破解家用WI-FI密码 - WPA/WPA2加密
  16. DevOps到底是什么意思?-小白收藏
  17. 【转】python获取计算股票技术指标DIF/DEA/MACD
  18. 中文版redis官方命令大全
  19. CSS设置多张网络图片垂直居中展示
  20. 青龙毛毛快手极速版0407修复版

热门文章

  1. routersploit 路由器漏洞检测及利用框架
  2. mysql 区分大小写(大小写敏感)配置
  3. Linux C 获取本地 ip mac 域名对应 ip
  4. 屏蔽JS错误提示 IE6 IE8
  5. TCP/IP详解--学习笔记(7)-广播和多播,IGMP协议
  6. mysql 查看某个表缓存情况_MySQL 查询缓存 QUERY_CACHE
  7. golang exec cmd pipeline zombile 进程
  8. Linux内核网络(一)——初探内核网络
  9. mysql中如何将一个表中的部分记录合并,MySQL数据库将多条记录的单个字段合并成一条记录_MySQL...
  10. python2 float类型_Python学习笔记2:基本数据类型