项目中 SimpleDateFormat 的正确使用

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

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

一、问题场景重现

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

public class SimpleDateFormatTest {

    /** 日期格式化类. */
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);
} public static void main(String[] args) throws InterruptedException {
/** 单线程下测试. */
System.out.println(sdf.format(new Date()));
}
}

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

    public static void main(String[] args) throws InterruptedException {
/** 单线程下测试. */
System.out.println(sdf.format(new Date())); /** 多线程下测试. */
ExecutorService service = Executors.newFixedThreadPool(100);
for (int i = 0; i < 20; i++) {
service.execute(() -> {
for (int j = 0; j < 10; j++) {
try {
System.out.println(parse("2018-01-02 09:45:59"));
} catch (ParseException e) {
e.printStackTrace();
}
}
});
}
// 等待上述的线程执行完后
service.shutdown();
service.awaitTermination(1, TimeUnit.DAYS);
}

控制台打印结果:

部分线程获取的时间不对,部分线程直接报 java.lang.NumberFormatException:multiple points 错,线程直接挂死了。

二、多线程不安全原因

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

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

    // Called from Format after creating a FieldDelegate
private StringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {
// Convert input date to time field list
calendar.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),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源码上作者也给过我们提示:

* 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.

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

三、解决方案

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

    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);
}

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

synchronized 大法好

    public static String formatDate(Date date) throws ParseException {
//return sdf.format(date);
synchronized (sdf) {
return sdf.format(date);
}
}
public static Date parse(String strDate) throws ParseException {
//return sdf.parse(strDate);
synchronized (sdf) {
return sdf.parse(strDate);
}
}

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

ThreadLocal

    /* ThreadLocal */
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
}; public static String formatDate(Date date) throws ParseException {
//return sdf.format(date);
/*synchronized (sdf) {
return sdf.format(date);
}*/
return threadLocal.get().format(date);
} public static Date parse(String strDate) throws ParseException {
//return sdf.parse(strDate);
/*synchronized (sdf) {
return sdf.parse(strDate);
}*/
return threadLocal.get().parse(strDate);
}

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

基于JDK1.8的DateTimeFormatter

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

public class SimpleDateFormatTest8 {
// 新建 DateTimeFormatter 类
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private static String formatDate(LocalDateTime dateTime) {
return FORMATTER.format(dateTime);
} private static LocalDateTime parse(String dateNow) {
return LocalDateTime.parse(dateNow, FORMATTER);
} public static void main(String[] args) throws InterruptedException { ExecutorService service = Executors.newFixedThreadPool(100);
for (int i = 0; i < 20; i++) {
service.execute(() -> {
for (int j = 0; j < 10; j++) {
try {
System.out.println(parse(formatDate(LocalDateTime.now())));
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
// 等待上述的线程执行完后
service.shutdown();
service.awaitTermination(1, TimeUnit.DAYS);
}
}

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

项目中 SimpleDateFormat 的正确使用的更多相关文章

  1. 2020年在项目中使用MVVM正确姿势,你用对了吗?

    最近看到了几篇与 Jetpack MVVM 有关到文章,使我不禁也想淌一下这场混水.我是在 2017 年下半年接触的 Jetpack 的那套开发工具,并且后来一直将其作为开发的主要框架.在这段时间的使 ...

  2. 未找到导入的项目,请确认 <Import> 声明中的路径正确

    当使用vs出现下列情况: D:\xxxx\Web\Web.csproj : error  : 无法读取项目文件“Web.csproj”. D:\xxxx\WebServiceManager\Web\W ...

  3. 升级CUDA版本导致VS2010错误:未找到导入的项目XXX,请确认<Import>声明中的路径正确,且磁盘上存在该文件

    转自:http://www.cnblogs.com/yeahgis/p/3853420.html VS2010错误:未找到导入的项目XXX,请确认<Import>声明中的路径正确,且磁盘上 ...

  4. vs未找到导入的项目,请确认 <Import> 声明中的路径正确

    当使用vs出现下列情况: D:\xxxx\Web\Web.csproj : error  : 无法读取项目文件“Web.csproj”. D:\xxxx\WebServiceManager\Web\W ...

  5. 关于如何正确地在android项目中添加第三方jar包

    在android项目中添加第三方jar包虽然不是一个很复杂的问题,但是确实给很多开发者带来了不小的困扰.我自己就曾经碰到过calss not found exception.error inflati ...

  6. 如何正确的在项目中接入微信JS-SDK

    微信JS-SDK的功能 如果你点进来,那么我相信你应该知道微信的JS-SDK可以用来做什么了.微信的官方文档描述如下. 微信JS-SDK是微信公众平台面向网页开发者提供的基于微信内的网页开发工具包. ...

  7. 在vue项目中封装echarts的正确姿势

    为什么需要封装echarts 每个开发者在制作图表时都需要从头到尾书写一遍完整的option配置,十分冗余 在同一个项目中,各类图表设计十分相似,甚至是相同,没必要一直做重复工作 可能有一些开发者忘记 ...

  8. vs2013打开 2010项目时: 请确认 <Import> 声明中的路径正确,且磁盘上存在该文件

    错误原因:原来2010项目中使用的路径是 v11.0,但是我的 C:\Program Files (x86)\MSBuild\12.0\ 是 12.0,所以该成 12.0 解决办法:将项目文件(.cs ...

  9. android 项目中log信息的正确处理

    我们在做项目中,调试程序的时候通常是debug或者打log,特别是看一些json数据之类的,都喜欢打log看看,可是在项目上线时log信息一定不能被打印出来,大家能够平时下载一些做的烂的app,连接e ...

随机推荐

  1. oracle查询当前系统时间前10天的数据

    select * from eo_c_order t where t.create_time>systimestamp-interval'1'day; 转载于:https://www.cnblo ...

  2. javascript中Function、ArrowFunction和GeneratorFunction介绍

    ECMAScript规范中对Function的文档描述,我认为是ECMAScript规范中最复杂也是最不好理解的一部分,它涉及到了各方面.光对Function就分了Function Definitio ...

  3. 图论——Tarjan 初步 DFS序+时间戳+欧拉序

    一.什么是DFS序: DFS序是按照先序遍历,先遍历根节点然后依次遍历左子树,右子树的过程,每次遇到新的节点就把新访问节点加到序列中,代码如下: int DFSrk[100000]; int cnt= ...

  4. keras-深度学习处理文本数据

    深度学习用于自然语言处理是将模式识别应用于单词.句子和段落,这与计算机视觉是将模式识别应用于像素大致相同.深度学习模型不会接收原始文本作为输入,它只能处理数值张量,因此我们必须将文本向量化(vecto ...

  5. libevent(三)event_base

    libevent能够处理三种事件: I/O.定时器.信号. event_base 统一管理所有事件. struct event_base { const struct eventop *evsel; ...

  6. django3开发完整博客带评价

    纯django开发最完美博客 2020年5月打造最时尚博客系统教程 为了学习速度,集中精力学习django和博客开发, 没有使用其它框架,也没有使用css预处理等 这样学起来最方便, 博客前后端都完成 ...

  7. java基础篇 之 非静态内部类

    什么是非静态内部类: public class Outer { Outer() { System.out.println("我是外部类"); } class Inner { Inn ...

  8. SpringCloudStream学习(二)RabbitMQ中的交换机跟工作模式

    知识储备: 交换机: ​ RabbitMQ中有4中交换机,分别是 (FANOUT)扇形交换机: 扇形交换机是最基本的交换机类型,它所能做的事情非常简单---广播消息.扇形交换机会把能接收到的消息全部发 ...

  9. 【Spark】RDD的依赖关系和缓存相关知识点

    文章目录 RDD的依赖关系 宽依赖 窄依赖 血统 RDD缓存 概述 缓存方式 RDD的依赖关系 RDD和它依赖的父RDD的关系有两种不同的类型,即窄依赖(narrow dependency) 和宽依赖 ...

  10. 【编程之美】常用于单片机的接口适配器模式C语言实现

    版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://www.cnblogs.com/lihuidashen/p/128750 ...