SimpleDateFormat 如何安全的使用?

前言

阿里巴巴开发手册详尽版里:

image.png

看到这条我立马就想起了以前有个项目里面就犯了这个错误,记得当时是这样写的:

private static final SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");

所以才认真的去研究下这个 SimpleDateFormat,所以才有了这篇文章。

它是谁?

想必大家对 SimpleDateFormat 并不陌生。
SimpleDateFormat 是 Java 中一个非常常用的类,他是以区域敏感的方式格式化和解析日期的具体类。
它允许格式化 (date -> text)、语法分析 (text -> date)和标准化。

SimpleDateFormat 允许以任何用户指定的日期-时间格式方式启动。 但是,建议使用 DateFormat 中的 getTimeInstance、 getDateInstance 或 getDateTimeInstance 方法来创建一个日期-时间格式。 这几个方法会返回一个默认的日期/时间格式。 你可以根据需要用 applyPattern 方法修改格式方式。

日期时间格式

日期和时间格式由 日期和时间模式字符串 指定。在 日期和时间模式字符串 中,未加引号的字母 'A' 到 'Z' 和 'a' 到 'z' 被解释为模式字母,用来表示日期或时间字符串元素。文本可以使用单引号 (') 引起来,以免进行解释。所有其他字符均不解释,只是在格式化时将它们简单复制到输出字符串。

简单的讲:这些 A ——Z,a —— z 这些字母(不被单引号包围的)会被特殊处理替换为对应的日期时间,其他的字符串还是原样输出。

日期和时间模式(注意大小写,代表的含义是不同的)如下:

image.png

怎么使用?

日期/时间格式模版样例:(给的时间是:2001-07-04 12:08:56 U.S. Pacific Time time zone)

image.png

import java.text.SimpleDateFormat;
import java.util.Date;

public class FormatDateTime {
    public static void main(String[] args) {
        SimpleDateFormat myFmt = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒");
        SimpleDateFormat myFmt1 = new SimpleDateFormat("yy/MM/dd HH:mm");
        SimpleDateFormat myFmt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//等价于now.toLocaleString()
        SimpleDateFormat myFmt3 = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒 E ");
        SimpleDateFormat myFmt4 = new SimpleDateFormat("一年中的第 D 天 一年中第w个星期 一月中第W个星期 在一天中k时 z时区");
        Date now = new Date();
        System.out.println(myFmt.format(now));
        System.out.println(myFmt1.format(now));
        System.out.println(myFmt2.format(now));
        System.out.println(myFmt3.format(now));
        System.out.println(myFmt4.format(now));
        System.out.println(now.toGMTString());
        System.out.println(now.toLocaleString());
        System.out.println(now.toString());
    }
}

结果是:

2019年08月15日 13时46分19秒
19/08/15 13:46
2019-08-15 13:46:19
2019年08月15日 13时46分19秒 星期四 
一年中的第 227 天 一年中第33个星期 一月中第3个星期 在一天中13时 CST时区
15 Aug 2019 05:46:19 GMT
2019-8-15 13:46:19
Thu Aug 15 13:46:19 CST 2019

间 Date 参数)。

上面的是日期转换成自己想要的字符串格式。下面反过来,将字符串类型装换成日期类型:

public static void main(String[] args) {
        String time1 = "2018年06月19日 23时10分05秒";
        String time2 = "18/06/19 23:10";
        String time3 = "2018-06-19 23:10:05";
        String time4 = "2018年06月19日 23时10分05秒 星期二";

        SimpleDateFormat myFmt = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒");
        SimpleDateFormat myFmt1 = new SimpleDateFormat("yy/MM/dd HH:mm");
        SimpleDateFormat myFmt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//等价于now.toLocaleString()
        SimpleDateFormat myFmt3 = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒 E");

        Date date1 = null;
        try {
            date1 = myFmt.parse(time1);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        System.out.println(date1);

        Date date2 = null;
        try {
            date2 = myFmt1.parse(time2);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        System.out.println(date2);

        Date date3 = null;
        try {
            date3 = myFmt2.parse(time3);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        System.out.println(date3);

        Date date4 = null;
        try {
            date4 = myFmt3.parse(time4);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        System.out.println(date4);
    }

结果是:

Tue Jun 19 23:10:05 CST 2018
Tue Jun 19 23:10:00 CST 2018
Tue Jun 19 23:10:05 CST 2018
Tue Jun 19 23:10:05 CST 2018

这个转换方法也很简单。但是不要高兴的太早,主角不在这。

线程不安全

image.png

在 SimpleDateFormat 类的 JavaDoc 中,描述了该类不能够保证线程安全,建议为每个线程创建单独的日期/时间格式实例,如果多个线程同时访问一个日期/时间格式,它必须在外部进行同步。
那么在多线程环境下调用 format() 和 parse() 方法应该使用同步代码来避免问题。
下面我们通过一个具体的场景来一步步的深入学习和理解SimpleDateFormat 类。

1、每个线程创建单独的日期/时间格式实例

大量的创建 SimpleDateFormat 实例对象,然后再丢弃这个对象,占用大量的内存和 JVM 空间。

2、创建一个静态的 SimpleDateFormat 实例,在使用时直接使用这个实例进行操作(我当时就是这么干的😄)
private static final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = new Date();
df.format(date);

当然,这个方法的确很不错,在大部分的时间里面都会工作得很好,但一旦在生产环境中一定负载情况下时,这个问题就出来了。
他会出现各种不同的情况,比如转化的时间不正确,比如报错,比如线程被挂死等等。
我们看下面的测试用例,拿事实说话:

Exception in thread "Thread-0" Exception in thread "Thread-1" java.lang.NumberFormatException: multiple points
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
    at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
    at java.lang.Double.parseDouble(Double.java:538)
    at java.text.DigitList.getDouble(DigitList.java:169)
    at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    at java.text.DateFormat.parse(DateFormat.java:364)
    at com.nuanshui.manage.controller.manage.thirdparty.merchant.DateUtils.parse(DateUtils.java:16)
    at com.nuanshui.manage.controller.manage.thirdparty.merchant.DateUtilsTest$TestSimpleDateFormatThreadSafe.run(DateUtilsTest.java:16)
java.lang.NumberFormatException: multiple points
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
    at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
    at java.lang.Double.parseDouble(Double.java:538)
    at java.text.DigitList.getDouble(DigitList.java:169)
    at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    at java.text.DateFormat.parse(DateFormat.java:364)
    at com.nuanshui.manage.controller.manage.thirdparty.merchant.DateUtils.parse(DateUtils.java:16)
    at com.nuanshui.manage.controller.manage.thirdparty.merchant.DateUtilsTest$TestSimpleDateFormatThreadSafe.run(DateUtilsTest.java:16)
Thread-2:Sun Jun 20 01:18:20 CST 2021
Thread-2:Wed Jun 20 01:18:20 CST 2018
Thread-2:Wed Jun 20 01:18:20 CST 2018
Thread-2:Wed Jun 20 01:18:20 CST 2018

说明:Thread-1和Thread-0报java.lang.NumberFormatException: multiple points错误,直接挂死,没起来;Thread-2 虽然没有挂死,但输出的时间是有错误的,比如我们输入的时间是:2018-06-20 01:18:20 ,当会输出:Sat Jun 20 01:18:20 CST 2201 这样的灵异事件。

为什么会出现线程不安全的问题呢?

下面我们通过看 JDK 源码来看看为什么 SimpleDateFormat 和 DateFormat 类不是线程安全的真正原因:

SimpleDateFormat 继承了 DateFormat,在 DateFormat 中定义了一个 protected 属性的 Calendar 类的对象:calendar。只是因为 Calendar 类的概念复杂,牵扯到时区与本地化等等,JDK 的实现中使用了成员变量来传递参数,这就造成在多线程的时候会出现错误。

在 SimpleDateFormat 中的 format 方法源码中:

image.png
线程 1 调用 format 方法,改变了 calendar 这个字段。
线程 1 中断了。
线程 2 开始执行,它也改变了 calendar。
线程 2 中断了。
线程 1 回来了

此时,calendar 已然不是它所设的值,而是走上了线程 2 设计的道路。如果多个线程同时争抢 calendar 对象,则会出现各种问题,时间不对,线程挂死等等。

分析一下 format 的实现,我们不难发现,用到成员变量 calendar,唯一的好处,就是在调用 subFormat 时,少了一个参数,却带来了许多的问题。其实,只要在这里用一个局部变量,一路传递下去,所有问题都将迎刃而解。

这个问题背后隐藏着一个更为重要的问题--无状态:无状态方法的好处之一,就是它在各种环境下,都可以安全的调用。衡量一个方法是否是有状态的,就看它是否改动了其它的东西,比如全局变量,比如实例的字段。format 方法在运行过程中改动了 SimpleDateFormat 的 calendar 字段,所以,它是有状态的。

这也同时提醒我们在开发和设计系统的时候注意下一下三点:
1.自己写公用类的时候,要对多线程调用情况下的后果在注释里进行明确说明
2.多线程环境下,对每一个共享的可变变量都要注意其线程安全性
3.我们的类和方法在做设计的时候,要尽量设计成无状态的

解决方法
  • 1、需要的时候创建新实例

说明:在需要用到 SimpleDateFormat 的地方新建一个实例,不管什么时候,将有线程安全问题的对象由共享变为局部私有都能避免多线程问题,不过也加重了创建对象的负担。在一般情况下,这样其实对性能影响比不是很明显的。

  • 2、使用同步:同步 SimpleDateFormat 对象
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class DateSyncUtil {

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

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

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

说明:当线程较多时,当一个线程调用该方法时,其他想要调用此方法的线程就要 block 等待,多线程并发量大的时候会对性能有一定的影响

  • 3、使用 ThreadLocal
public class ConcurrentDateUtil {

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

说明:使用 ThreadLocal, 也是将共享变量变为独享,线程独享肯定能比方法独享在并发环境中能减少不少创建对象的开销。如果对性能要求比较高的情况下,一般推荐使用这种方法。

Java 8 中的解决办法
Java 8 提供了新的日期时间 API,其中包括用于日期时间格式化的 DateTimeFormatter,它与 SimpleDateFormat 最大的区别在于:DateTimeFormatter 是线程安全的,而 SimpleDateFormat 并不是线程安全。

DateTimeFormatter 如何使用:

    public static void main(String[] arg0) {
        // 解析日期
        String dateStr = "2018年06月20日";
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
        LocalDate localDate = LocalDate.parse(dateStr, formatter);
        System.out.println("localDate:" + localDate);
        //日期转换为字符串
        LocalDateTime now = LocalDateTime.now();
        DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy年MM月dd日 hh:mm a");
        String nowStr = now.format(format);
        System.out.println("nowStr:" + nowStr);
    }

由 DateTimeFormatter 的静态方法 ofPattern() 构建日期格式,LocalDateTime 和 LocalDate 等一些表示日期或时间的类使用 parse 和 format 方法把日期和字符串做转换。

使用新的 API,整个转换过程都不需要考虑线程安全的问题。

总结
SimpleDateFormat 是线程不安全的类,多线程环境下注意线程安全问题.
如果是 Java 8 ,建议使用 DateTimeFormatter 代替 SimpleDateFormat。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容