编程语言还在使用SimpleDateFormat?

    作者:佚名更新于: 2020-02-27 14:00:49

    大神带你学编程,欢迎选课

    还在使用SimpleDateFormat?你的项目崩没?编程语言(programming language)是一种被标准化的交流技巧,用来向计算机发出指令,定义计算机程序,让程序员能够准确地定义计算机所需要使用的数据,并精确地定义在不同情况下所应当采取的行动的一种计算机语言。 编程语言可以分成机器语言、汇编语言、高级语言三大类。计算机领域已发明了上千不同的编程语言,而且每年仍有新的编程语言诞生。

    在多线程情况下,会出现异常,想必有经验的小伙伴也遇到过。下面我们就来分析分析SimpleDateFormat为什么不安全?是怎么引发的?以及多线程下有那些SimpleDateFormat的解决方案?

    编程语言还在使用SimpleDateFormat?_Access 视频_Access 开发视频_Access 课程视频_课课家

    一.前言

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

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

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

    二.问题场景复现

    一般我们使用SimpleDateFormat的时候会把它定义为一个静态变量,避免频繁创建它的对象实例,如下代码: 

    1. public class SimpleDateFormatTest {  
    2.         private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
    3.         public static String formatDate(Date date) throws ParseException {  
    4.             return sdf.format(date);  
    5.         }  
    6.         public static Date parse(String strDate) throws ParseException {  
    7.             return sdf.parse(strDate);  
    8.         }  
    9.         public static void main(String[] args) throws InterruptedException, ParseException {  
    10.             System.out.println(sdf.format(new Date()));  
    11.         }  
    12.     } 

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

    1. public static void main(String[] args) throws InterruptedException, ParseException {  
    2.             ExecutorService service = Executors.newFixedThreadPool(100);  
    3.             for (int i = 0; i < 20; i++) {  
    4.                 service.execute(() -> {  
    5.                     for (int j = 0; j < 10; j++) {  
    6.                         try {  
    7.                             System.out.println(parse("2018-01-02 09:45:59"));  
    8.                         } catch (ParseException e) {  
    9.                             e.printStackTrace();  
    10.                         }  
    11.                     }  
    12.                 });  
    13.             }  
    14.             // 等待上述的线程执行完  
    15.             service.shutdown(); 
    16.             service.awaitTermination(1, TimeUnit.DAYS);  
    17.         } 

    控制台打印结果:

    你看这不崩了?部分线程获取的时间不对,部分线程直接报 java.lang.NumberFormatException:multiple points错,线程直接挂死了。

    三.多线程不安全原因

    因为我们把SimpleDateFormat定义为静态变量,那么多线程下SimpleDateFormat的实例就会被多个线程共享,B线程会读取到A线程的时间,就会出现时间差异和其它各种问题。SimpleDateFormat和它继承的DateFormat类也不是线程安全的

    来看看SimpleDateFormat的format()方法的源码   

    1. // Called from Format after creating a FieldDelegate  
    2.       private StringBuffer format(Date date, StringBuffer toAppendTo, 
    3.                                   FieldDelegate delegate) {  
    4.           // Convert input date to time field list  
    5.           calendar.setTime(date);  
    6.           boolean useDateFormatSymbolsuseDateFormatSymbols = useDateFormatSymbols();  
    7.           for (int i = 0; i < compiledPattern.length; ) {  
    8.               int tag = compiledPattern[i] >>> 8;  
    9.               int count = compiledPattern[i++] & 0xff;  
    10.               if (count == 255) {  
    11.                   count = compiledPattern[i++] << 16;  
    12.                   count |= compiledPattern[i++];  
    13.               }  
    14.               switch (tag) {  
    15.               case TAG_QUOTE_ASCII_CHAR:  
    16.                   toAppendTo.append((char)count);  
    17.                   break;  
    18.               case TAG_QUOTE_CHARS:  
    19.                   toAppendTo.append(compiledPattern, i, count);  
    20.                   i += count;  
    21.                   break;  
    22.               default:  
    23.                   subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);  
    24.                   break;  
    25.               }  
    26.           }  
    27.           return toAppendTo;  
    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源码上作者也给过我们提示: 

    1. * Date formats are not synchronized.  
    2.    * It is recommended to create separate format instances for each thread.  
    3.    * If multiple threads Access a format concurrently, it must be synchronized  
    4.    * externally. 

    意思就是

    日期格式不同步。

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

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

    四.解决方案

    只在需要的时候创建新实例,不用static修饰   

    1. public static String formatDate(Date date) throws ParseException {  
    2.           SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
    3.           return sdf.format(date);  
    4.       } 
    5.       public static Date parse(String strDate) throws ParseException {  
    6.           SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
    7.           return sdf.parse(strDate);  
    8.       } 

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

    synchronized大法好     

    1. private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
    2.         public static String formatDate(Date date) throws ParseException {  
    3.             synchronized(sdf){  
    4.                 return sdf.format(date);  
    5.             }  
    6.         }  
    7.         public static Date parse(String strDate) throws ParseException {  
    8.             synchronized(sdf){  
    9.                 return sdf.parse(strDate);  
    10.             }  
    11.         } 

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

    ThreadLocal     

    1. private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {  
    2.            @Override  
    3.            protected DateFormat initialValue() {  
    4.                return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
    5.            }  
    6.        };  
    7.        public static Date parse(String dateStr) throws ParseException {  
    8.            return threadLocal.get().parse(dateStr);  
    9.        }  
    10.        public static String format(Date date) {  
    11.            return threadLocal.get().format(date);  
    12.        } 

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

    基于JDk1.8的DateTimeFormatter

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

    1. public class SimpleDateFormatTest {  
    2.         private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");  
    3.         public static String formatDate2(LocalDateTime date) {  
    4.             return formatter.format(date);  
    5.         }  
    6.         public static LocalDateTime parse2(String dateNow) {  
    7.             return LocalDateTime.parse(dateNow, formatter);  
    8.         }  
    9.         public static void main(String[] args) throws InterruptedException, ParseException {  
    10.             ExecutorService service = Executors.newFixedThreadPool(100);  
    11.             // 20个线程  
    12.             for (int i = 0; i < 20; i++) {  
    13.                 service.execute(() -> {  
    14.                     for (int j = 0; j < 10; j++) {  
    15.                         try {  
    16.                             System.out.println(parse2(formatDate2(LocalDateTime.now())));  
    17.                         } catch (Exception e) {  
    18.                             e.printStackTrace();  
    19.                         }  
    20.                     }  
    21.                 });  
    22.             }  
    23.             // 等待上述的线程执行完  
    24.             service.shutdown();  
    25.             service.awaitTermination(1, TimeUnit.DAYS);  
    26.         }  
    27.     } 

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

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

    1. * This class is immutable and thread-safe. 
      编程语言往往使程序员能够比使用机器语言更准确地表达他们所想表达的目的。对那些从事计算机科学的人来说,懂得程序设计语言是十分重要的,因为在当今所有的计算都需要程序设计语言才能完成。

课课家教育

未登录