阅读本文大概须要 3.2 分钟。java
前言 程序员
平常开发中,咱们常常须要使用时间相关类,想必你们对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
关于线程名称
再次简单进一步解读下:
链表类型的阻塞队列,而咱们看其构造函数发现,默认队列大小是整数的最大值!!!
因此若是请求太多,队列极可能就耗费内存很是大致使OOM。
可是他们的线程数是固定的,并且通常不会太大,因此不会由于建立过多线程而致使OOM。
其中第最大线程池大小是整数的最大值,所以线程可能不断建立,乃至到整数的最大值个线程,很容易致使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
往期精彩回顾