SimpleDateFormat 如何安全的使用?

转载请务必注明原创地址为:http://www.54tianzhisheng.cn/2018/06/19/SimpleDateFormat/

在看的过程中有这么一条:

【强制】SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static,必须加锁,或者使用 DateUtils 工具类。

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

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

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

它是谁?

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

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

日期时间格式

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

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

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

怎么使用?

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

使用方法:

 1import java.text.SimpleDateFormat;
 2import java.util.Date;
 3/**
 4 * Created by zhisheng_tian on 2018/6/19
 5 */
 6public class FormatDateTime {
 7    public static void main(String[] args) {
 8        SimpleDateFormat myFmt = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒");
 9        SimpleDateFormat myFmt1 = new SimpleDateFormat("yy/MM/dd HH:mm");
10        SimpleDateFormat myFmt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//等价于now.toLocaleString()
11        SimpleDateFormat myFmt3 = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒 E ");
12        SimpleDateFormat myFmt4 = new SimpleDateFormat("一年中的第 D 天 一年中第w个星期 一月中第W个星期 在一天中k时 z时区");
13        Date now = new Date();
14        System.out.println(myFmt.format(now));
15        System.out.println(myFmt1.format(now));
16        System.out.println(myFmt2.format(now));
17        System.out.println(myFmt3.format(now));
18        System.out.println(myFmt4.format(now));
19        System.out.println(now.toGMTString());
20        System.out.println(now.toLocaleString());
21        System.out.println(now.toString());
22    }
23}

结果是:

12018年06月19日 23时10分05秒
218/06/19 23:10
32018-06-19 23:10:05
42018年06月19日 23时10分05秒 星期二
5一年中的第 170 天 一年中第25个星期 一月中第4个星期 在一天中23时 CST时区
619 Jun 2018 15:10:05 GMT
72018-6-19 23:10:05
8Tue Jun 19 23:10:05 CST 2018

使用方法很简单,就是先自己定义好时间/日期模版,然后调用 format 方法(传入一个时间 Date 参数)。

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

 1import java.text.ParseException;
 2import java.text.SimpleDateFormat;
 3import java.util.Date;
 4/**
 5 * Created by zhisheng_tian on 2018/6/19
 6 */
 7public class StringFormatDate {
 8
 9    public static void main(String[] args) {
10        String time1 = "2018年06月19日 23时10分05秒";
11        String time2 = "18/06/19 23:10";
12        String time3 = "2018-06-19 23:10:05";
13        String time4 = "2018年06月19日 23时10分05秒 星期二";
14
15        SimpleDateFormat myFmt = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒");
16        SimpleDateFormat myFmt1 = new SimpleDateFormat("yy/MM/dd HH:mm");
17        SimpleDateFormat myFmt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//等价于now.toLocaleString()
18        SimpleDateFormat myFmt3 = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒 E");
19
20        Date date1 = null;
21        try {
22            date1 = myFmt.parse(time1);
23        } catch (ParseException e) {
24            e.printStackTrace();
25        }
26        System.out.println(date1);
27
28        Date date2 = null;
29        try {
30            date2 = myFmt1.parse(time2);
31        } catch (ParseException e) {
32            e.printStackTrace();
33        }
34        System.out.println(date2);
35
36        Date date3 = null;
37        try {
38            date3 = myFmt2.parse(time3);
39        } catch (ParseException e) {
40            e.printStackTrace();
41        }
42        System.out.println(date3);
43
44        Date date4 = null;
45        try {
46            date4 = myFmt3.parse(time4);
47        } catch (ParseException e) {
48            e.printStackTrace();
49        }
50        System.out.println(date4);
51    }
52}

结果是:

1Tue Jun 19 23:10:05 CST 2018
2Tue Jun 19 23:10:00 CST 2018
3Tue Jun 19 23:10:05 CST 2018
4Tue Jun 19 23:10:05 CST 2018

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

线程不安全

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

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

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

2、创建一个静态的 SimpleDateFormat 实例,在使用时直接使用这个实例进行操作(我当时就是这么干的?)

1private static final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
2Date date = new Date();
3df.format(date);

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

 1import java.text.ParseException;
 2import java.text.SimpleDateFormat;
 3import java.util.Date;
 4/**
 5 * Created by zhisheng_tian on 2018/6/20
 6 */
 7public class DateUtils {
 8    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
 9
10    public static String formatDate(Date date) throws ParseException {
11        return sdf.format(date);
12    }
13
14    public static Date parse(String strDate) throws ParseException {
15        return sdf.parse(strDate);
16    }
17}
 1import java.text.ParseException;
 2/**
 3 * Created by zhisheng_tian on 2018/6/20
 4 */
 5public class DateUtilsTest {
 6    public static class TestSimpleDateFormatThreadSafe extends Thread {
 7        @Override
 8        public void run() {
 9            while (true) {
10                try {
11                    this.join(2000);
12                } catch (InterruptedException e1) {
13                    e1.printStackTrace();
14                }
15                try {
16                    System.out.println(this.getName() + ":" + DateUtils.parse("2018-06-20 01:18:20"));
17                } catch (ParseException e) {
18                    e.printStackTrace();
19                }
20            }
21        }
22    }
23    public static void main(String[] args) {
24        for (int i = 0; i < 3; i++) {
25            new TestSimpleDateFormatThreadSafe().start();
26        }
27    }
28}

运行结果如下:

 1Exception in thread "Thread-0" Exception in thread "Thread-1" java.lang.NumberFormatException: For input string: ""
 2    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
 3    at java.lang.Long.parseLong(Long.java:601)
 4    at java.lang.Long.parseLong(Long.java:631)
 5    at java.text.DigitList.getLong(DigitList.java:195)
 6    at java.text.DecimalFormat.parse(DecimalFormat.java:2051)
 7    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
 8    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
 9    at java.text.DateFormat.parse(DateFormat.java:364)
10    at com.zhisheng.demo.date.DateUtils.parse(DateUtils.java:19)
11    at com.zhisheng.demo.date.DateUtilsTest$TestSimpleDateFormatThreadSafe.run(DateUtilsTest.java:19)
12java.lang.NumberFormatException: For input string: ".1818"
13    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
14    at java.lang.Long.parseLong(Long.java:578)
15    at java.lang.Long.parseLong(Long.java:631)
16    at java.text.DigitList.getLong(DigitList.java:195)
17    at java.text.DecimalFormat.parse(DecimalFormat.java:2051)
18    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
19    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
20    at java.text.DateFormat.parse(DateFormat.java:364)
21    at com.zhisheng.demo.date.DateUtils.parse(DateUtils.java:19)
22    at com.zhisheng.demo.date.DateUtilsTest$TestSimpleDateFormatThreadSafe.run(DateUtilsTest.java:19)
23Thread-2:Sat Jun 20 01:18:20 CST 2201
24Thread-2:Wed Jun 20 01:18:20 CST 2018
25Thread-2:Wed Jun 20 01:18:20 CST 2018
26Thread-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 这样的灵异事件。

Why?

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

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

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

在 SimpleDateFormat 中的 format 方法源码中:

 1@Override
 2public StringBuffer format(Date date, StringBuffer toAppendTo,FieldPosition pos) {
 3  pos.beginIndex = pos.endIndex = 0;
 4  return format(date, toAppendTo, pos.getFieldDelegate());
 5}
 6// Called from Format after creating a FieldDelegate
 7private StringBuffer format(Date date, StringBuffer toAppendTo,FieldDelegate delegate) {
 8  // Convert input date to time field list
 9  calendar.setTime(date);
10  boolean useDateFormatSymbols = useDateFormatSymbols();
11  for (int i = 0; i < compiledPattern.length; ) {
12    int tag = compiledPattern[i] >>> 8;
13    int count = compiledPattern[i++] & 0xff;
14    if (count == 255) {
15      count = compiledPattern[i++] << 16;
16      count |= compiledPattern[i++];
17    }
18
19    switch (tag) {
20      case TAG_QUOTE_ASCII_CHAR:
21        toAppendTo.append((char)count);
22        break;
23      case TAG_QUOTE_CHARS:
24        toAppendTo.append(compiledPattern, i, count);
25        i += count;
26        break;
27      default:
28        subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
29        break;
30    }
31  }
32  return toAppendTo;
33}

calendar.setTime(date) 这条语句改变了 calendar,稍后,calendar 还会用到(在 subFormat 方法里),而这就是引发问题的根源。想象一下,在一个多线程环境下,有两个线程持有了同一个 SimpleDateFormat 的实例,分别调用format 方法:

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

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

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

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

这也同时提醒我们在开发和设计系统的时候注意下一下三点:

1.自己写公用类的时候,要对多线程调用情况下的后果在注释里进行明确说明

2.多线程环境下,对每一个共享的可变变量都要注意其线程安全性

3.我们的类和方法在做设计的时候,要尽量设计成无状态的

解决方法

1、需要的时候创建新实例

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

2、使用同步:同步 SimpleDateFormat 对象

 1import java.text.ParseException;
 2import java.text.SimpleDateFormat;
 3import java.util.Date;
 4
 5public class DateSyncUtil {
 6
 7    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
 8
 9    public static String formatDate(Date date) throws ParseException {
10        synchronized(sdf) {
11            return sdf.format(date);
12        }  
13    }
14
15    public static Date parse(String strDate) throws ParseException {
16        synchronized(sdf) {
17            return sdf.parse(strDate);
18        }
19    }
20}

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

3、使用 ThreadLocal

 1import java.text.DateFormat;
 2import java.text.ParseException;
 3import java.text.SimpleDateFormat;
 4import java.util.Date;
 5
 6public class ConcurrentDateUtil {
 7
 8    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
 9        @Override
10        protected DateFormat initialValue() {
11            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
12        }
13    };
14
15    public static Date parse(String dateStr) throws ParseException {
16        return threadLocal.get().parse(dateStr);
17    }
18
19    public static String format(Date date) {
20        return threadLocal.get().format(date);
21    }
22}

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

Java 8 中的解决办法

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

DateTimeFormatter 如何使用:

解析日期

1String dateStr= "2018年06月20日";
2DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");   
3LocalDate date= LocalDate.parse(dateStr, formatter);

日期转换为字符串

1LocalDateTime now = LocalDateTime.now();  
2DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy年MM月dd日 hh:mm a");
3String nowStr = now .format(format);

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

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

总结

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

参考资料

http://www.cnblogs.com/peida/archive/2013/05/31/3070790.htm

原文发布于微信公众号 - zhisheng(zhisheng_blog)

原文发表时间:2018-06-20

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏程序生活

Leetcode-Easy 543. Diameter of Binary Tree

543. Diameter of Binary Tree 描述: 求二叉树最长路径长度 ? 思路: 深度优先搜索 代码 # Definit...

3184
来自专栏Bingo的深度学习杂货店

Q112 Path Sum

Given a binary tree and a sum, determine if the tree has a root-to-leaf path suc...

3497
来自专栏一名叫大蕉的程序员

大数据计数原理1+0=1这你都不会算(四)No.52

这是本坑的第四篇,之前已经说了关于 HashSet 、BitMap 、Bloom Filter 布隆过滤器了,本篇主要讲B-树。要是还不知道前面讲了啥的,可以点...

2117
来自专栏大闲人柴毛毛

剑指offer代码分析——面试题13在O(1)内删除链表结点

本题详细解析都已在代码中注释了: /** * 给一个单链表,头指针为first,请用O(1)时间删除其中节点p * @author chibozhou *...

3765
来自专栏陈树义

Java ConcurrentModificationException异常原因和解决方法

Java ConcurrentModificationException异常原因和解决方法   在前面一篇文章中提到,对Vector、ArrayList在迭代的...

3874
来自专栏尾尾部落

[剑指offer] 重建二叉树

输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6...

1491
来自专栏Android知识点总结

看得见的数据结构Android版之二分搜索树篇

974
来自专栏老马说编程

(32) 剖析日期和时间 / 计算机程序的思维逻辑

本节和下节,我们讨论在Java中如何进行日期和时间相关的操作。 日期和时间是一个比较复杂的概念,Java API中对它的支持不是特别好,有一个第三方的类库反而特...

21310
来自专栏Vamei实验室

纸上谈兵: 左倾堆 (leftist heap)

我们之前讲解了堆(heap)的概念。堆是一个优先队列。每次从堆中取出的元素都是堆中优先级最高的元素。 在之前的文章中,我们基于完全二叉树(complete bi...

3289
来自专栏计算机视觉与深度学习基础

Leetcode 34 Search for a Range

Given a sorted array of integers, find the starting and ending position of a gi...

2169

扫码关注云+社区

领取腾讯云代金券