阅读本文大概需要 3.2 分钟。

前言

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

下面我们就来分析分析SimpleDateFormat为什么不安全?是怎么引发的?以及多线程下有那些SimpleDateFormat的解决方案?

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

问题复现

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

打印一下结果:

是不是感觉没什么毛病?相信大多数人都是这样使用的,也包括我。在单线程下自然没毛病了,但是运用到多线程下就有大问题了。

测试下:

控制台打印结果:

你看结果,发现了什么?直接崩了,部分线程获取的时间不对,部分线程报java.lang.NumberFormatException:multiple points错,线程直接挂死了。还有部分线程报empty String错,值有问题。

多线程不安全原因

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

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

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

翻译过来的意思就是:

日期格式未同步。

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

解决方案

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

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

采用Synchronized方式

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

ThreadLocal

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

基于JDK1.8的DateTimeFormatter

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

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

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

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

知识扩展

在上述代码中,我们通过创建一个线程池,来实现多线程循环打印日期的操作,但是我们创建方式你有没有留意。

ExecutorService executorService = Executors.newFixedThreadPool(100);

当你IDEA安装了阿里巴巴的代码规范检查插件时,使用Executors来创建线程池的话,会出现提示让你手动创建线程池。

因此,我们可以将创建线程池的代码改成:

ExecutorService executorService = new ThreadPoolExecutor(100, 100,0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());

但是又会有提示,建议要为线程池中的线程设置名称:

改造之后的代码为:

ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("thread-call-runner-%d").build();
ExecutorService executorService = new ThreadPoolExecutor(100, 100,0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());

这里会有个问题,ThreadFactoryBuilder()在JDK1.8及之后被去除了,所以如果你的JDK低于1.8即可使用该方法,等于或高于1.8可采取其他方式设置线程名称,也可用其他方式手动创建线程池。

为什么要这样做

我们参考阿里巴巴的Java开发手册内容:

关于Executors

关于线程名称

再次简单进一步解读下:

  • newFixedThreadPool和newSingleThreadExecutor 由于最后一个参数即工作队列是

链表类型的阻塞队列,而我们看其构造函数发现,默认队列大小是整数的最大值!!!

所以如果请求太多,队列很可能就耗费内存非常大导致OOM。

但是他们的线程数是固定的,而且一般不会太大,所以不会因为创建过多线程而导致OOM。

  • 再来看下newCachedThreadPool和newScheduledThreadPool

其中第最大线程池大小是整数的最大值,因此线程可能不断创建,乃至到整数的最大值个线程,很容易导致OOM。其中工作队列使用的是 SynchronousQueue<E>,源码头部的注释中有说明(截取的部分)。

A {@linkplain BlockingQueue blocking queue} in which each insert operation must wait for a corresponding remove operation by another thread, and vice versa.

该类型的阻塞队列每一个插入操作必须等待对应的元素被另一个线程所移除,反之亦然。

因此阻塞队列不会无限拓展而导致OOM。

当我们学习和理解一些原则的同时,多注重源码分析!!!

·END·

程序员的成长之路

路虽远,行则必至

本文原发于 同名微信公众号「程序员的成长之路」,回复「1024」你懂得,给个赞呗。

微信ID:cxydczzl

往期精彩回顾

还在使用SimpleDateFormat?的更多相关文章

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

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

  2. 还在用SimpleDateFormat?Java8都发布N年了,转LocalDateTime吧

    前言 Java8发布,已有数年之久,但是发现很多人都还是坚持着用SimpleDateFormat和Date进行时间操作.SimpleDateFormat这个类不是线程安全的,在使用的时候稍不注意,就会 ...

  3. 还在使用SimpleDateFormat?你的项目崩没?

    如果是在多线程环境下,就需要注意了. 要点: 1.加Synchronized同步: 2.使用ThreadLocal: 3.jdk8使用DateTimeFormatter替代SimpleDateForm ...

  4. 推荐几个牛逼的 IDEA 插件,还带动图!

    阅读本文大概需要 2.3 分钟. 作者:纪莫, cnblogs.com/jimoer 这里只是推荐一下好用的插件,具体的使用方法不一一详细介绍. JRebel for IntelliJ 一款热部署插件 ...

  5. 为什么阿里Java规约要求谨慎使用SimpleDateFormat

    前言 在阿里Java开发规约中,有强制性的提到SimpleDateFormat 是线程不安全的类 ,在使用的时候应当注意线程安全问题,如下: 其实之前已经介绍过使用JDK1.8的DateTimeFor ...

  6. 为什么SimpleDateFormat不是线程安全的?

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

  7. 一个完整的 Web 请求到底发生了什么

    阅读本文大概需要 7 分钟. 一.从输入一个网址开始 当我们在浏览器输入一个网址,然后按下回车,接下来浏览器显示了页面.网速好的话这之间可能就一秒,但在这一秒内到底发生了什么? 本文主要内容是试图记录 ...

  8. 选择 25k 的 996 还是 18k 的 965

    阅读本文大概需要 3.7 分钟. 文中部分内容来源:http://h5ip.cn/hSDk 最近的 996.ICU 话题持续在火爆,一般热点新闻的热度持续时间为一星期,这次是程序们的集体发声导致,戳中 ...

  9. 阿里巴巴的26款超神Java开源项目!

    来源:https://segmentfault.com/a/1190000017346799 1.分布式应用服务开发的一站式解决方案 Spring Cloud Alibaba Spring Cloud ...

随机推荐

  1. nginx访问502 gateway,*1 connect() failed (111: Connection refused) while connecting to upstream

    安装好nginx,php环境后,配置虚拟主机,结果访问后就报502 gateway,查看日志文件后,显示错误如下: 2019/04/29 16:24:39 [error] 19433#19433: * ...

  2. 如何用命令将本地项目上传到git[z]

    1.(先进入项目文件夹)通过命令 git init 把这个目录变成git可以管理的仓库 git init 2.把文件添加到版本库中,使用命令 git add .添加到暂存区里面去,不要忘记后面的小数点 ...

  3. MySQL联接查询算法(NLJ、BNL、BKA、HashJoin)

    一.联接过程介绍 为了后面一些测试案例,我们事先创建了两张表,表数据如下:   1 2 3 4 CREATE TABLE t1 (m1 int, n1 char(1)); CREATE TABLE t ...

  4. 判断作业完成之 读取log 脚本

    tail 用于读取文件末尾 -n 后边加行数 -f 为持续追踪,实时输出 NUMECA 输出最后两行 ...done in 727.84-sec (STARTING NUMECA FLOW SOLVE ...

  5. 一年web网站测试总结

    1. 页面链接检查 每一个链接是否都有对应的页面,并且页面之间切换正确.可以使用一些工具,如LinkBotPro.File-AIDCS.HTML Link Validater.Xenu等工具.Link ...

  6. 关于webpack使用的一些问题

    1.镜像安装 npm安装webpack慢的爆炸,如果不能FQ,试下下面的国内良心镜像,浑身都舒爽了. npm config set registry https://registry.npm.taob ...

  7. P3806 【模板】点分治1

    一道淀粉质的模版题,开始是暴力 #include <bits/stdc++.h> #define up(i,l,r) for(register int i = (l); i <= ( ...

  8. C语言闰年问题程序框图

    判定2000-2500年中的每一年是否为闰年,并将结果输出. 先分析闰年成立条件: 1)能被4整除,但不能被100整除的年份都是闰年: (2)能被400整除的年份是闰年: #include<st ...

  9. MySQL PROFILE 跟踪语句各阶段性能开销

    PROFILE  可以跟踪查询语句各个阶段 Time,IO,CPU,MEMORY 等资源使用情况,比较详细.所以系统一般不会记录太多.启用是全局的,所以每个连接都保持语句的资源使用情况. 查看 PRO ...

  10. 2019.03.29 bzoj3323: [Scoi2013]多项式的运算(非旋treap)

    传送门 题意:定义一个无穷项的多项式f(x)f(x)f(x),初始各项系数都为0,现在有几种操作 将xLx^LxL到xRx^RxR这些项的系数乘上某个定值v 将xLx^LxL到xRx^RxR这些项的系 ...