還在使用SimpleDateFormat?你的項(xiàng)目崩沒?
一.前言
日常開發(fā)中,我們經(jīng)常需要使用時(shí)間相關(guān)類,說到時(shí)間相關(guān)類,想必大家對(duì)SimpleDateFormat并不陌生。主要是用它進(jìn)行時(shí)間的格式化輸出和解析,挺方便快捷的,但是SimpleDateFormat并不是一個(gè)線程安全的類。在多線程情況下,會(huì)出現(xiàn)異常,想必有經(jīng)驗(yàn)的小伙伴也遇到過。下面我們就來分析分析SimpleDateFormat為什么不安全?是怎么引發(fā)的?以及多線程下有那些SimpleDateFormat的解決方案?
先看看《阿里巴巴開發(fā)手冊(cè)》對(duì)于SimpleDateFormat是怎么看待的:
公眾號(hào)后臺(tái)回復(fù)"阿里巴巴開發(fā)手冊(cè)"獲取《阿里巴巴開發(fā)手冊(cè)》v 1.4.0
二.問題場(chǎng)景復(fù)現(xiàn)
一般我們使用SimpleDateFormat的時(shí)候會(huì)把它定義為一個(gè)靜態(tài)變量,避免頻繁創(chuàng)建它的對(duì)象實(shí)例,如下代碼:
- 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, ParseException {
- System.out.println(sdf.format(new Date()));
- }
- }
是不是感覺沒什么毛病?單線程下自然沒毛病了,都是運(yùn)用到多線程下就有大問題了。測(cè)試下:
- public static void main(String[] args) throws InterruptedException, ParseException {
- 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();
- }
- }
- });
- }
- // 等待上述的線程執(zhí)行完
- service.shutdown();
- service.awaitTermination(1, TimeUnit.DAYS);
- }
控制臺(tái)打印結(jié)果:
你看這不崩了?部分線程獲取的時(shí)間不對(duì),部分線程直接報(bào) java.lang.NumberFormatException:multiple points錯(cuò),線程直接掛死了。
三.多線程不安全原因
因?yàn)槲覀儼裇impleDateFormat定義為靜態(tài)變量,那么多線程下SimpleDateFormat的實(shí)例就會(huì)被多個(gè)線程共享,B線程會(huì)讀取到A線程的時(shí)間,就會(huì)出現(xiàn)時(shí)間差異和其它各種問題。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 useDateFormatSymbolsuseDateFormatSymbols = 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方法實(shí)際操作的就是Calendar。
因?yàn)槲覀兟暶鱏impleDateFormat為static變量,那么它的Calendar變量也就是一個(gè)共享變量,可以被多個(gè)線程訪問。
假設(shè)線程A執(zhí)行完calendar.setTime(date),把時(shí)間設(shè)置成2019-01-02,這時(shí)候被掛起,線程B獲得CPU執(zhí)行權(quán)。線程B也執(zhí)行到了calendar.setTime(date),把時(shí)間設(shè)置為2019-01-03。線程掛起,線程A繼續(xù)走,calendar還會(huì)被繼續(xù)使用(subFormat方法),而這時(shí)calendar用的是線程B設(shè)置的值了,而這就是引發(fā)問題的根源,出現(xiàn)時(shí)間不對(duì),線程掛死等等。
其實(shí)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.
意思就是
日期格式不同步。
建議為每個(gè)線程創(chuàng)建單獨(dú)的格式實(shí)例。
如果多個(gè)線程同時(shí)訪問一種格式,則必須在外部同步該格式。
四.解決方案
只在需要的時(shí)候創(chuàng)建新實(shí)例,不用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);
- }
如上代碼,僅在需要用到的地方創(chuàng)建一個(gè)新的實(shí)例,就沒有線程安全問題,不過也加重了創(chuàng)建對(duì)象的負(fù)擔(dān),會(huì)頻繁地創(chuàng)建和銷毀對(duì)象,效率較低。
synchronized大法好
- private static final 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);
- }
- }
簡(jiǎn)單粗暴,synchronized往上一套也可以解決線程安全問題,缺點(diǎn)自然就是并發(fā)量大的時(shí)候會(huì)對(duì)性能有影響,線程阻塞。
ThreadLocal
- 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可以確保每個(gè)線程都可以得到單獨(dú)的一個(gè)SimpleDateFormat的對(duì)象,那么自然也就不存在競(jìng)爭(zhēng)問題了。
基于JDK1.8的DateTimeFormatter
也是《阿里巴巴開發(fā)手冊(cè)》給我們的解決方案,對(duì)之前的代碼進(jìn)行改造:
- public class SimpleDateFormatTest {
- private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
- public static String formatDate2(LocalDateTime date) {
- return formatter.format(date);
- }
- public static LocalDateTime parse2(String dateNow) {
- return LocalDateTime.parse(dateNow, formatter);
- }
- public static void main(String[] args) throws InterruptedException, ParseException {
- ExecutorService service = Executors.newFixedThreadPool(100);
- // 20個(gè)線程
- for (int i = 0; i < 20; i++) {
- service.execute(() -> {
- for (int j = 0; j < 10; j++) {
- try {
- System.out.println(parse2(formatDate2(LocalDateTime.now())));
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- });
- }
- // 等待上述的線程執(zhí)行完
- service.shutdown();
- service.awaitTermination(1, TimeUnit.DAYS);
- }
- }
運(yùn)行結(jié)果就不貼了,不會(huì)出現(xiàn)報(bào)錯(cuò)和時(shí)間不準(zhǔn)確的問題。
DateTimeFormatter源碼上作者也加注釋說明了,他的類是不可變的,并且是線程安全的。
- * This class is immutable and thread-safe.