SimpleDateFormat 的线程安全问题与 ThreadLocal
文章目录
从 SimpleDateFormat
的线程安全说起
SimpleDateFormat
是 Java 中非常常用的一个类,用于解析和格式化日期字符串。这个类想必大家都有用过,但是 SimpleDateFormat
在多线程环境中并不是线程安全的。我刚知道这一点的时候也觉得很奇怪,因为 SimpleDateFormat
就是个工具类而已,为什么还会存在线程安全的问题呢。下面我们具体来看一下。
首先,我们写个简单的例子来验证一下:
|
|
输出的部分内容是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Thread-9 Fri Jan 01 10:24:00 CST 2016 Thread-1 Sat Feb 25 00:48:00 CST 20162017 Thread-5 Sat Feb 25 00:48:00 CST 20162017 Exception in thread "Thread-4" Exception in thread "Thread-6" java.lang.NumberFormatException: For input string: "2002.E20022E" at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043) 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:2162) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514) at java.text.DateFormat.parse(DateFormat.java:364) at DateUtil.parse(DateUtil.java:24) at DateUtil$2.run(DateUtil.java:45) |
可以看到,其中的有些线程抛出了运行时异常 NumberFormatException
,而有些则输出了奇怪的日期结果 20162017
。很明显,在多线程环境中 SimpleDateFormat
确实存在线程安全的问题。
为什么 SimpleDateFormat
不是线程安全的?
在 JDK 的文档中提到了 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.
那么,原因又是什么呢?我们来简单地看一下 SimpleDateFormat 的源码:
|
|
可以看到,在 format()
方法中先将日期存放到一个 Calendar
对象中,而这个 Calender
对象在 SimpleDateFormat
中还是以成员变量存在的。在随后调用 subFormat()
时会再次用到成员变量 calendar
。这就是引发问题的根源。在 parse()
方法中也会存在相应的问题。
试想,在多线程环境下,如果两个线程都使用同一个 SimpleDateFormat
实例,那么就有可能存在其中一个线程修改了 calendar
后紧接着另一个线程也修改了 calendar
,那么随后第一个线程用到 calendar
时已经不是它所期待的值了。
SimpleDateFormat
其实是有状态的,它使用一个 Calendar
成员变量来保存状态;如果要求 SimpleDateFormat
的 parse()
和 format()
是线程安全的,那么它其实应该是无状态的。将 Calendar
对象作为局部变量,内部在进行方法调用时每次都把它作为参数进行传递,其实就应该可以做到线程安全了。JDK 中 SimpleDateFormat
的实现之所以没有这样做可能是出于性能上的考虑,可以节约每次方法调用时都要创建 Calendar
对象的开销。但这种有状态的设计在某些场景下却反而带来了使用上的不便。
如何保证 SimpleDateFormat
的线程安全
最简单的方法就是每次要使用 SimpleDateFormat
时都创建一个局部的 SimpleDateFormat
对象。局部变量,自然就不存在线程安全的问题了。但如果需要频繁进行调用的话,每次都要创建新的对象,开销太大。
第二种方式,就是对 SimpleDateFormat
进行加锁,这样可以确保同一时间只有一个线程可以持有锁,进而解决线程安全的问题。但是这种方法在多线程竞争激烈的时候会带来效率问题。
第三种方式,就是使用 ThreadLocal
。 ThreadLocal
可以确保每个线程都可以得到单独的一个 SimpleDateFormat
的对象,那么自然也就不存在竞争问题了。
|
|
用 ThreadLocal
来实现其实是有点类似于缓存的思路,每个线程都有一个独享的对象,避免了频繁创建对象,也避免了多线程的竞争。
也可以将 SimpleDateFormat
对象的创建进行延迟加载:
|
|
文章作者 jrthe42
原始链接 https://blog.jrwang.me/2016/java-simpledateformat-multithread-threadlocal/
上次更新 2019-09-12
许可协议 CC BY-NC-ND 4.0