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


文本公众号来源:Felix周(id:felix_space)  作者:Felix 


一篇关于SimpleDateFormat干货分享给大家!

640?wx_fmt=jpeg



前言


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

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

640?wx_fmt=png

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



问题场景复现


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

 
 
  1. public class SimpleDateFormatTest { class SimpleDateFormatTest {

  2.     private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

  3.     public static String formatDate(Date date) throws ParseException {public static String formatDate(Date date) throws ParseException {
  4.         return sdf.format(date);return sdf.format(date);
  5.     }

  6.     public static Date parse(String strDate) throws ParseException {public static Date parse(String strDate) throws ParseException {
  7.         return sdf.parse(strDate);return sdf.parse(strDate);
  8.     }

  9.     public static void main(String[] args) throws InterruptedException, ParseException {public static void main(String[] args) throws InterruptedException, ParseException {

  10.         System.out.println(sdf.format(new Date()));System.out.println(sdf.format(new Date()));

  11.     }
  12. }

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

 
 

  1.         ExecutorService service = Executors.newFixedThreadPool(100);ExecutorService service = Executors.newFixedThreadPool(100);

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

控制台打印结果:

640?wx_fmt=png

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



多线程不安全原因


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

来看看SimpleDateFormatformat()方法的源码

 
 
  1.     // Called from Format after creating a FieldDelegate// Called from Format after creating a FieldDelegate
  2.     private StringBuffer format(Date date, StringBuffer toAppendTo,private StringBuffer format(Date date, StringBuffer toAppendTo,
  3.                                 FieldDelegate delegate) {FieldDelegate delegate) {
  4.         // Convert input date to time field list// Convert input date to time field list
  5.         calendar.setTime(date);

  6.         boolean useDateFormatSymbols = useDateFormatSymbols();boolean useDateFormatSymbols = useDateFormatSymbols();

  7.         for (int i = 0; i < compiledPattern.length; ) {for (int i = 0; i < compiledPattern.length; ) {
  8.             int tag = compiledPattern[i] >>> 8;int tag = compiledPattern[i] >>> 8;
  9.             int count = compiledPattern[i++] & 0xff;int count = compiledPattern[i++] & 0xff;
  10.             if (count == 255) {if (count == 255) {
  11.                 count = compiledPattern[i++] << 16;16;
  12.                 count |= compiledPattern[i++];
  13.             }

  14.             switch (tag) {switch (tag) {
  15.             case TAG_QUOTE_ASCII_CHAR:case TAG_QUOTE_ASCII_CHAR:
  16.                 toAppendTo.append((char)count);char)count);
  17.                 break;break;

  18.             case TAG_QUOTE_CHARS:case TAG_QUOTE_CHARS:
  19.                 toAppendTo.append(compiledPattern, i, count);
  20.                 i += count;
  21.                 break;break;

  22.             default:default:
  23.                 subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
  24.                 break;break;
  25.             }
  26.         }
  27.         return toAppendTo;return toAppendTo;
  28.     }

注意, calendar.setTime(date)SimpleDateFormatformat方法实际操作的就是Calendar

因为我们声明SimpleDateFormatstatic变量,那么它的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.Date formats are not synchronized.
  2. * It is recommended to create separate format instances for each thread.It is recommended to create separate format instances for each thread.
  3. * If multiple threads access a format concurrently, it must be synchronizedIf multiple threads access a format concurrently, it must be synchronized
  4. * externally.

意思就是

日期格式不同步。

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

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




解决方案


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

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

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

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

synchronized大法好

 
 
  1.     private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

  2.     public static String formatDate(Date date) throws ParseException {public static String formatDate(Date date) throws ParseException {
  3.         synchronized(sdf){synchronized(sdf){
  4.             return sdf.format(date);return sdf.format(date);
  5.         }
  6.     }

  7.     public static Date parse(String strDate) throws ParseException {public static Date parse(String strDate) throws ParseException {
  8.         synchronized(sdf){synchronized(sdf){
  9.             return sdf.parse(strDate);return sdf.parse(strDate);
  10.         }
  11.     }

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

ThreadLocal

 
 
  1.     private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
  2.         @Override@Override
  3.         protected DateFormat initialValue() {protected DateFormat initialValue() {
  4.             return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  5.         }
  6.     };

  7.     public static Date parse(String dateStr) throws ParseException {public static Date parse(String dateStr) throws ParseException {
  8.         return threadLocal.get().parse(dateStr);return threadLocal.get().parse(dateStr);
  9.     }

  10.     public static String format(Date date) {public static String format(Date date) {
  11.         return threadLocal.get().format(date);return threadLocal.get().format(date);
  12.     }

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

基于JDK1.8的DateTimeFormatter

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

 
 
  1. public class SimpleDateFormatTest { class SimpleDateFormatTest {

  2.     private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

  3.     public static String formatDate2(LocalDateTime date) {public static String formatDate2(LocalDateTime date) {
  4.         return formatter.format(date);return formatter.format(date);
  5.     }

  6.     public static LocalDateTime parse2(String dateNow) {public static LocalDateTime parse2(String dateNow) {
  7.         return LocalDateTime.parse(dateNow, formatter);return LocalDateTime.parse(dateNow, formatter);
  8.     }

  9.     public static void main(String[] args) throws InterruptedException, ParseException {public static void main(String[] args) throws InterruptedException, ParseException {

  10.         ExecutorService service = Executors.newFixedThreadPool(100);ExecutorService service = Executors.newFixedThreadPool(100);

  11.         // 20个线程// 20个线程
  12.         for (int i = 0; i < 20; i++) {for (int i = 0; i < 20; i++) {
  13.             service.execute(() -> {
  14.                 for (int j = 0; j < 10; j++) {for (int j = 0; j < 10; j++) {
  15.                     try {try {
  16.                         System.out.println(parse2(formatDate2(LocalDateTime.now())));System.out.println(parse2(formatDate2(LocalDateTime.now())));
  17.                     } catch (Exception e) {catch (Exception e) {
  18.                         e.printStackTrace();
  19.                     }
  20.                 }
  21.             });
  22.         }
  23.         // 等待上述的线程执行完// 等待上述的线程执行完
  24.         service.shutdown();
  25.         service.awaitTermination(1, TimeUnit.DAYS);1, TimeUnit.DAYS);


  26.     }
  27. }

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

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

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


OK,现在是不是可以对你项目里的日期工具类进行一波优化了呢?




640?wx_fmt=gif



推荐阅读

1.Java程序员的Spring学习指南!

2.Synchronized锁在Spring事务管理下,为啥还线程不安全?

3.金三银四,二月就要准备!



Java3yI D:java3y

如果你喜欢本文,请分享到朋友圈,想要获得更多信息,请关注我!


欢迎扫描下方二维码,持续关注,喜欢请点好看 640?wx_fmt=jpeg


Java3y(id:java3y




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值