来看看SimpleDateFormat的线程安全问题。
程序猿日常与时间打交道是必不可少的,而针对date类型的转换,用的最多的应该就是SimpleDateFormat,然而,此类并不是线程安全的,此文会列出证明其线程不安全和其原因,以及如何解决。
来看日常使用SimpDateFormat的简单代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class SimpDateFormatTest { private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss" ); public static String formatDate (Date date) { return sdf.format(date); } public static Date parse (String timeStr) { Date date = new Date(); try { date = sdf.parse(timeStr); } catch (ParseException e) { e.printStackTrace(); } return date; } public static void main (String[] args) { parse("2019-04-11 13:48:08" ); } }
单线程情况下没有什么问题,来看多线程任务下的执行情况,将结果打印到控制台:
1 2 3 4 5 6 7 8 9 10 public static void main (String[] args) throws InterruptedException { ExecutorService service = Executors.newFixedThreadPool(100 ); for (int i = 0 ; i < 20 ; i++) { service.execute(() -> { System.out.println(parse("2019-04-11 10:48:08" )); }); } service.shutdown(); service.awaitTermination(1 ,TimeUnit.SECONDS); }
这里用20个线程来做String与Date的转换,看看控制台:
可以看到,多线程任务执行下转换的日期的不匹配,没毛病吧,再看看format方法多线程环境下的打印:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static void main (String[] args) throws InterruptedException { Date date = parse("1998-04-11 19:00:00" ); Date date1 = parse("2019-04-11 10:00:00" ); ExecutorService service = Executors.newFixedThreadPool(100 ); for (int i = 0 ; i < 10 ; i++) { final int j = i; service.execute(() -> { if (j % 2 == 0 ) { System.out.println(formatDate(date)); }else { System.out.println(formatDate(date1)); } }); } service.shutdown(); service.awaitTermination(1 ,TimeUnit.SECONDS); }
控制台打印如下:
你看2019多过分,平白无故比人家1998多打印3次,值大压值小吗?肯定不是,来看看原因。
为何非线程安全 因为把SimpleDateFormat定义为静态变量,那么多线程下SimpleDateFormat的实例就会被多个线程共享,B线程会读取到A线程的时间,就会出现时间差异和其它各种问题。SimpleDateFormat和它继承的DateFormat类也不是线程安全的
来看看SimpleDateFormat的format()方法的源码
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 29 30 31 32 33 private StringBuffer format (Date date, StringBuffer toAppendTo, FieldDelegate delegate) { calendar.setTime(date); boolean useDateFormatSymbols = 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方法实际操作的就是Calendar。
因为声明SimpleDateFormat为static变量,那么它的Calendar变量也就是一个共享变量,可以被多个线程访问。
假设线程A执行完calendar.setTime(date),把时间设置成1998-04-11 19:00:00,这时候被挂起,线程B获得CPU执行权。线程B也执行到了calendar.setTime(date),把时间设置为2019-04-11 10:00:00。线程挂起,线程A继续走,calendar还会被继续使用(subFormat方法),而这时calendar用的是线程B设置的值了,而这就是引发问题的根源,出现时间不对,线程挂死等等。
SimpleDateFormat上面给出的注释也明确的指出:
1 2 3 4 * 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.
如何解决
不在类中用static修饰,直接把SimpleDateFormat放到方法中来创建实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static String formatDate (Date date) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss" ); return sdf.format(date); } public static Date parse (String timeStr) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss" ); Date date = new Date(); try { date = sdf.parse(timeStr); } catch (ParseException e) { e.printStackTrace(); } return date; }
如上代码,仅在需要用到的地方创建一个新的实例,就没有线程安全问题,不过也加重了创建对象的负担,会频繁地创建和销毁对象,效率较低。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static String formatDate (Date date) { synchronized (sdf) { return sdf.format(date); } } public static Date parse (String timeStr) { synchronized (sdf) { Date date = new Date(); try { date = sdf.parse(timeStr); } catch (ParseException e) { e.printStackTrace(); } return date; } }
简单粗暴,synchronized往上一套也可以解决线程安全问题,缺点自然就是并发量大的时候会对性能有影响,线程阻塞。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){ @Override protected DateFormat initialValue () { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss" ); } }; public static String formatDate (Date date) { return threadLocal.get().format(date); } public static Date parse (String timeStr) { Date date = new Date(); try { date = threadLocal.get().parse(timeStr); } catch (ParseException e) { e.printStackTrace(); } return date; }
ThreadLocal会为每一个被其修饰的变量创建一个副本在线程中使用,因此可以解决线程安全问题,缺点是占用内存,当然这点内存对java来讲貌似可以忽略不计。
基于JDK1.8的DateTimeFormatter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private static final DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" ); public static String formatDate (LocalDateTime date) { return format.format(date); } public static LocalDateTime parse (String timeStr) { return LocalDateTime.parse(timeStr,format); } public static void main (String[] args) throws InterruptedException { ExecutorService service = Executors.newFixedThreadPool(100 ); for (int i = 0 ; i < 100 ; i++) { service.execute(() -> { System.out.println(parse("2019-04-11 10:52:16" )); }); } service.shutdown(); service.awaitTermination(1 , TimeUnit.SECONDS); }
也是《阿里巴巴开发手册》给出的解决方案,运行结果就不贴了,不会出现报错和时间不准确的问题,不过在运用中还是不习惯用DateTimeFormatter,或许下个项目中可以用上呢。