深圳幻海软件技术有限公司 欢迎您!

还在使用SimpleDateFormat?你的项目崩没?

2023-02-27

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

一.前言

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

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

公众号后台回复"阿里巴巴开发手册"获取《阿里巴巴开发手册》v 1.4.0

二.问题场景复现

一般我们使用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, ParseException {  
            System.out.println(sdf.format(new Date()));  
        }  
    } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

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

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();  
                        }  
                    }  
                });  
            }  
            // 等待上述的线程执行完  
            service.shutdown(); 
            service.awaitTermination(1, TimeUnit.DAYS);  
        } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

控制台打印结果:

你看这不崩了?部分线程获取的时间不对,部分线程直接报 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 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;  
      } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.

注意, 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. 
  • 1.
  • 2.
  • 3.
  • 4.

意思就是

日期格式不同步。

建议为每个线程创建单独的格式实例。 

如果多个线程同时访问一种格式,则必须在外部同步该格式。

四.解决方案

只在需要的时候创建新实例,不用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);  
      } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

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

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);  
            }  
        } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

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

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);  
       } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

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

基于JDK1.8的DateTimeFormatter

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

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个线程  
            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();  
                        }  
                    }  
                });  
            }  
            // 等待上述的线程执行完  
            service.shutdown();  
            service.awaitTermination(1, TimeUnit.DAYS);  
        }  
    } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.

运行结果就不贴了,不会出现报错和时间不准确的问题。

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

* This class is immutable and thread-safe. 
  • 1.