SimpleDateFormat是线程不安全的,不能多个线程公用。而FastDateFormat和Joda-Time都是线程安全的,可以放心使用。
SimpleDateFormat是JDK提供的,不需要依赖第三方jar包,而其他两种都得依赖第三方jar包。
FastDateFormat是apache的commons-lang3包提供的
Joda-Time需要依赖以下maven的配置(现在最新版本就是2.10.1)
<!-- https://mvnrepository.com/artifact/joda-time/joda-time -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.1</version>
</dependency>
一、SimpleDateFormat,线程不安全
SimpleDateFormat和FastDateFormat主要都是对时间的格式化
SimpleDateFormat在对时间进行格式化的方法format中,会先对calendar对象进行setTime的赋值,若是有多个线程同时操作一个SimpleDateFormat实例的话,就会对calendar的赋值进行覆盖,进而产生问题。
在format方法里,有这样一段代码:
private StringBuffer format(Date date, StringBuffer toAppendTo, FieldDelegate delegate) { // Convert input date to time field list 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)这条语句改变了calendar,稍后,calendar还会用到(在subFormat方法里),而这就是引发问题的根源。想象一下,在一个多线程环境下,有两个线程持有了同一个SimpleDateFormat的实例,分别调用format方法:
线程1调用format方法,改变了calendar这个字段。
中断来了。
线程2开始执行,它也改变了calendar。
又中断了。
线程1回来了,此时,calendar已然不是它所设的值,而是走上了线程2设计的道路。如果多个线程同时争抢calendar对象,则会出现各种问题,时间不对,线程挂死等等。
分析一下format的实现,我们不难发现,用到成员变量calendar,唯一的好处,就是在调用subFormat时,少了一个参数,却带来了这许多的问题。其实,只要在这里用一个局部变量,一路传递下去,所有问题都将迎刃而解。
这个问题背后隐藏着一个更为重要的问题--无状态:无状态方法的好处之一,就是它在各种环境下,都可以安全的调用。衡量一个方法是否是有状态的,就看它是否改动了其它的东西,比如全局变量,比如实例的字段。format方法在运行过程中改动了SimpleDateFormat的calendar字段,所以,它是有状态的。
有三种方法可以解决这个问题:
1、在每次需要使用的时候,进行SimpleDateFormat实例的创建,这种方式会导致创建一些对象实例,占用一些内存,不建议这样使用。
package com.peidasoft.dateformat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class DateUtil { 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); } }
说明:在需要用到SimpleDateFormat 的地方新建一个实例,不管什么时候,将有线程安全问题的对象由共享变为局部私有都能避免多线程问题,不过也加重了创建对象的负担。在一般情况下,这样其实对性能影响比不是很明显的。
2、使用同步的方式,在调用方法的时候加上synchronized,这样可以让线程调用方法时,进行加锁,也就是会造成线程间的互斥,对性能影响比较大。
package com.peidasoft.dateformat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class DateSyncUtil { private static 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); } } }
说明:当线程较多时,当一个线程调用该方法时,其他想要调用此方法的线程就要block,多线程并发量大的时候会对性能有一定的影响。
3、使用ThreadLocal进行保存,相当于一个线程只会有一个实例,进而减少了实例数量,也防止了线程间的互斥,推荐使用这种方式。
package com.peidasoft.dateformat; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class ConcurrentDateUtil { 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); } }
或者另一种写法:
package com.peidasoft.dateformat; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class ThreadLocalDateUtil { private static final String date_format = "yyyy-MM-dd HH:mm:ss"; private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(); public static DateFormat getDateFormat() { DateFormat df = threadLocal.get(); if(df==null){ df = new SimpleDateFormat(date_format); threadLocal.set(df); } return df; } public static String formatDate(Date date) throws ParseException { return getDateFormat().format(date); } public static Date parse(String strDate) throws ParseException { return getDateFormat().parse(strDate); } }
说明:使用ThreadLocal, 也是将共享变量变为独享,线程独享肯定能比方法独享在并发环境中能减少不少创建对象的开销。如果对性能要求比较高的情况下,一般推荐使用这种方法。
二、FastDateFormat,线程安全的,可以直接使用,不必考虑多线程的情况
FastDateFormat format = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss"); System.out.println(format.format(new Date())); // 可以使用DateFormatUtils类来操作,方法里面也是使用的FastDateFormat类来做的 System.out.println(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
三、Joda-Time,线程安全
Joda-Time与以上两种有所区别,不仅仅可以对时间进行格式化输出,而且可以生成瞬时时间值,并与Calendar、Date等对象相互转化,极大的方便了程序的兼容性。
Joda-Time的类具有不可变性,因此他们的实例是无法修改的,就跟String的对象一样。
这种不可变性提现在所有API方法中,这些方法返回的都是新的类实例,与原来实例不同。
以下是Joda-Time的一些使用方法
// 得到当前时间 Date currentDate = new Date(); DateTime dateTime = new DateTime(); // DateTime.now() System.out.println(currentDate.getTime()); System.out.println(dateTime.getMillis()); // 指定某一个时间,如2016-08-29 15:57:02 Date oneDate = new Date(1472457422728L); DateTime oneDateTime = new DateTime(1472457422728L); DateTime oneDateTime1 = new DateTime(2016, 8, 29, 15, 57, 2, 728); System.out.println(oneDate.toString()); System.out.println(oneDateTime.toString()); // datetime默认的输出格式为yyyy-MM-ddTHH:mm:ss.SSS System.out.println(oneDateTime1.toString("MM/dd/yyyy hh:mm:ss.SSSa")); // 直接就可以输出规定的格式 // DateTime和Date之间的转换 Date convertDate = new Date(); DateTime dt1 = new DateTime(convertDate); System.out.println(dt1.toString()); Date d1 = dt1.toDate(); System.out.println(d1.toString()); // DateTime和Calendar之间的转换 Calendar c1 = Calendar.getInstance(); DateTime dt2 = new DateTime(c1); System.out.println(dt2.toString()); Calendar c2 = dt2.toCalendar(null); // 默认时区Asia/Shanghai System.out.println(c2.getTimeZone()); // 时间格式化 DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss"); DateTime dt3 = DateTime.parse("2016-08-29 13:32:33", formatter); System.out.println(dt3.toString()); // 若是不指定格式,会采用默认的格式,yyyy-MM-ddTHH:mm:ss.SSS,若被解析字符串只到年月日,后面的时分秒会全部默认为0 DateTime dt4 = DateTime.parse("2016-08-29T"); System.out.println(dt4.toString()); // 输出locale 输出2016年08月29日 16:43:14 星期一 System.out.println(new DateTime().toString("yyyy年MM月dd日 HH:mm:ss EE", Locale.CHINESE)); // 计算两个日期间隔的天数 LocalDate start = new DateTime().toLocalDate(); LocalDate end = new LocalDate(2016, 8, 25); System.out.println(Days.daysBetween(start ,end).getDays()); // 这里要求start必须早于end,否则计算出来的是个负数 // 相同的还有间隔年数、月数、小时数、分钟数、秒数等计算 // 类如Years、Hours等 // 对日期的加减操作 DateTime dt5 = new DateTime(); dt5 = dt5.plusYears(1) // 增加年 .plusMonths(1) // 增加月 .plusDays(1) // 增加日 .minusHours(1) // 减小时 .minusMinutes(1) // 减分钟 .minusSeconds(1); // 减秒数 System.out.println(dt5.toString()); // 判断是否闰月 DateTime dt6 = new DateTime(); DateTime.Property month = dt6.monthOfYear(); System.out.println(month.isLeap());
原文链接:https://blog.csdn.net/zxh87/article/details/19414885,https://jjhpeopl.iteye.com/blog/2321528