还在使用SimpleDateFormat?
阅读本文大概需要 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·
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
现代C++之理解模板类型推断(template type deduction)
现代C++之理解模板类型推断(template type deduction)目录 ParamType是指针或者引用类型ParamType是一个Universal ReferenceParamType既不是指针也不是引用数组参数函数参数要点总结 正文 理解模板类型推断(template type deduction)我们往往不能理解一个复杂的系统是如何运作的,但是却知道这个系统能够做什么。C++的模板类型推断便是如此,把参数传递到模板函数往往能让程序员得到满意的结果,但是却不能够比较清晰的描述其中的推断过程。模板类型推断是现代C++中被广泛使用的关键字auto的基础。当在auto上下文中使用模板类型推断的时候,它不会像应用在模板中那么直观,所以理解模板类型推断是如何在auto中运作的就很重要了。 下面将详细讨论。看下面的伪代码: templatevoid f(ParamType param);通过下面的代码调用: f(expr); //call f with some expression在编译过程中编译器会使用expr推断两种类型:一个T的类型,一个是ParamType。而这两种类型...
- 下一篇
如何用阿里云搭建Web服务器
一、什么是服务器、云服务器 服务器:是一种高性能的计算机 云服务器:是一组集群服务器虚拟出类似独立服务器的部分,集群中每个服务器都有云服务器的一个镜像,从而大大提高了云服务器的稳定性。 以前都是传统服务器搭建Web运行环境,现在大多数都用云服务器搭建Web运行环境,因为云服务器具有高可靠性、高稳定性、高灵活性、高安全性、高性能,使用了云计算技术。 二、步骤 1、在阿里云买一个Linux系统的云服务器 2、在Windows上安装Xshell软件进行远程控制 3、在Linux服务器中安装jdk1.84、在Linux服务器中安装tomcat7.0 5、在Linux服务器中安装mysql 三、过程 1、在阿里云买一个Linux系统的云服务器2、在Windows上安装Xshell软件进行远程控制3、在Linux服务器中安装jdk
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS8编译安装MySQL8.0.19
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS8安装Docker,最新的服务器搭配容器使用
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- SpringBoot2整合Redis,开启缓存,提高访问速度
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS关闭SELinux安全模块