SimpleDateFormat 如何安全的使用?

使用方法:
import java.text.SimpleDateFormat;import java.util.Date;/*** Created by zhisheng_tian on 2018/6/19*/public class FormatDateTime {public static void main(String[] args) {SimpleDateFormat myFmt = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒");SimpleDateFormat myFmt1 = new SimpleDateFormat("yy/MM/dd HH:mm");SimpleDateFormat myFmt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//等价于now.toLocaleString()SimpleDateFormat myFmt3 = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒 E ");SimpleDateFormat myFmt4 = new SimpleDateFormat("一年中的第 D 天 一年中第w个星期 一月中第W个星期 在一天中k时 z时区");Date now = new Date();System.out.println(myFmt.format(now));System.out.println(myFmt1.format(now));System.out.println(myFmt2.format(now));System.out.println(myFmt3.format(now));System.out.println(myFmt4.format(now));System.out.println(now.toGMTString());System.out.println(now.toLocaleString());System.out.println(now.toString());}}
结果是:
2018年06月19日 23时10分05秒18/06/19 23:102018-06-19 23:10:052018年06月19日 23时10分05秒 星期二一年中的第 170 天 一年中第25个星期 一月中第4个星期 在一天中23时 CST时区19 Jun 2018 15:10:05 GMT2018-6-19 23:10:05Tue Jun 19 23:10:05 CST 2018
使用方法很简单,就是先自己定义好时间/日期模版,然后调用方法(传入一个时间 Date 参数) 。
上面的是日期转换成自己想要的字符串格式 。下面反过来,将字符串类型装换成日期类型:
import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.Date;/*** Created by zhisheng_tian on 2018/6/19*/public class StringFormatDate {public static void main(String[] args) {String time1 = "2018年06月19日 23时10分05秒";String time2 = "18/06/19 23:10";String time3 = "2018-06-19 23:10:05";String time4 = "2018年06月19日 23时10分05秒 星期二";SimpleDateFormat myFmt = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒");SimpleDateFormat myFmt1 = new SimpleDateFormat("yy/MM/dd HH:mm");SimpleDateFormat myFmt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//等价于now.toLocaleString()SimpleDateFormat myFmt3 = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒 E");Date date1 = null;try {date1 = myFmt.parse(time1);} catch (ParseException e) {e.printStackTrace();}System.out.println(date1);Date date2 = null;try {date2 = myFmt1.parse(time2);} catch (ParseException e) {e.printStackTrace();}System.out.println(date2);Date date3 = null;try {date3 = myFmt2.parse(time3);} catch (ParseException e) {e.printStackTrace();}System.out.println(date3);Date date4 = null;try {date4 = myFmt3.parse(time4);} catch (ParseException e) {e.printStackTrace();}System.out.println(date4);}}
结果是:
Tue Jun 19 23:10:05 CST 2018Tue Jun 19 23:10:00 CST 2018Tue Jun 19 23:10:05 CST 2018Tue Jun 19 23:10:05 CST 2018
这个转换方法也很简单 。但是不要高兴的太早,主角不在这 。
线程不安全
在类的中,描述了该类不能够保证线程安全,建议为每个线程创建单独的日期/时间格式实例,如果多个线程同时访问一个日期/时间格式,它必须在外部进行同步 。那么在多线程环境下调用 () 和 parse() 方法应该使用同步代码来避免问题 。下面我们通过一个具体的场景来一步步的深入学习和理解 类 。
1、每个线程创建单独的日期/时间格式实例
大量的创建实例对象,然后再丢弃这个对象,占用大量的内存和 JVM 空间 。
2、创建一个静态的实例,在使用时直接使用这个实例进行操作(我当时就是这么干的?)
private static final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");Date date = new Date();df.format(date);
当然,这个方法的确很不错,在大部分的时间里面都会工作得很好,但一旦在生产环境中一定负载情况下时,这个问题就出来了 。他会出现各种不同的情况,比如转化的时间不正确,比如报错,比如线程被挂死等等 。我们看下面的测试用例,拿事实说话:
import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.Date;/*** Created by zhisheng_tian on 2018/6/20*/public class DateUtils {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);}}
import java.text.ParseException;/*** Created by zhisheng_tian on 2018/6/20*/public class DateUtilsTest {public static class TestSimpleDateFormatThreadSafe extends Thread {@Overridepublic void run() {while (true) {try {this.join(2000);} catch (InterruptedException e1) {e1.printStackTrace();}try {System.out.println(this.getName() + ":" + DateUtils.parse("2018-06-20 01:18:20"));} catch (ParseException e) {e.printStackTrace();}}}}public static void main(String[] args) {for (int i = 0; i < 3; i++) {new TestSimpleDateFormatThreadSafe().start();}}}
运行结果如下:
Exception in thread "Thread-0" Exception in thread "Thread-1" java.lang.NumberFormatException: For input string: ""at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)at java.lang.Long.parseLong(Long.java:601)at java.lang.Long.parseLong(Long.java:631)at java.text.DigitList.getLong(DigitList.java:195)at java.text.DecimalFormat.parse(DecimalFormat.java:2051)at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)at java.text.DateFormat.parse(DateFormat.java:364)at com.zhisheng.demo.date.DateUtils.parse(DateUtils.java:19)at com.zhisheng.demo.date.DateUtilsTest$TestSimpleDateFormatThreadSafe.run(DateUtilsTest.java:19)java.lang.NumberFormatException: For input string: ".1818"at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)at java.lang.Long.parseLong(Long.java:578)at java.lang.Long.parseLong(Long.java:631)at java.text.DigitList.getLong(DigitList.java:195)at java.text.DecimalFormat.parse(DecimalFormat.java:2051)at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)at java.text.DateFormat.parse(DateFormat.java:364)at com.zhisheng.demo.date.DateUtils.parse(DateUtils.java:19)at com.zhisheng.demo.date.DateUtilsTest$TestSimpleDateFormatThreadSafe.run(DateUtilsTest.java:19)Thread-2:Sat Jun 20 01:18:20 CST 2201Thread-2:Wed Jun 20 01:18:20 CST 2018Thread-2:Wed Jun 20 01:18:20 CST 2018Thread-2:Wed Jun 20 01:18:20 CST 2018
说明:-1和-0报java.lang.n:错误,直接挂死,没起来;-2 虽然没有挂死,但输出的时间是有错误的,比如我们输入的时间是:2018-06-20 01:18:20 ,当会输出:Sat Jun 20 01:18:20 CST 2201 这样的灵异事件 。
为什么会出现线程不安全的问题呢?
下面我们通过看 JDK 源码来看看为什么和类不是线程安全的真正原因:
继承了 ,在中定义了一个属性的类的对象: 。只是因为类的概念复杂,牵扯到时区与本地化等等,JDK 的实现中使用了成员变量来传递参数,这就造成在多线程的时候会出现错误 。
在中的方法源码中:
@Overridepublic StringBuffer format(Date date, StringBuffer toAppendTo,FieldPosition pos) {pos.beginIndex = pos.endIndex = 0;return format(date, toAppendTo, pos.getFieldDelegate());}// Called from Format after creating a FieldDelegateprivate StringBuffer format(Date date, StringBuffer toAppendTo,FieldDelegate delegate) {// Convert input date to time field listcalendar.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;}
.(date) 这条语句改变了 ,稍后,还会用到(在方法里),而这就是引发问题的根源 。想象一下,在一个多线程环境下,有两个线程持有了同一个的实例,分别调用 方法:
线程 1 调用 format 方法,改变了 calendar 这个字段 。线程 1 中断了 。线程 2 开始执行,它也改变了 calendar 。线程 2 中断了 。线程 1 回来了
此时,已然不是它所设的值,而是走上了线程 2 设计的道路 。如果多个线程同时争抢对象,则会出现各种问题,时间不对,线程挂死等等 。
分析一下的实现,我们不难发现,用到成员变量 ,唯一的好处,就是在调用时,少了一个参数,却带来了许多的问题 。其实,只要在这里用一个局部变量,一路传递下去,所有问题都将迎刃而解 。
这个问题背后隐藏着一个更为重要的问题–无状态:无状态方法的好处之一,就是它在各种环境下,都可以安全的调用 。衡量一个方法是否是有状态的,就看它是否改动了其它的东西,比如全局变量,比如实例的字段 。方法在运行过程中改动了的字段,所以,它是有状态的 。
这也同时提醒我们在开发和设计系统的时候注意下一下三点:
1.自己写公用类的时候,要对多线程调用情况下的后果在注释里进行明确说明
2.多线程环境下,对每一个共享的可变变量都要注意其线程安全性
3.我们的类和方法在做设计的时候,要尽量设计成无状态的
解决方法
1、需要的时候创建新实例
说明:在需要用到的地方新建一个实例,不管什么时候,将有线程安全问题的对象由共享变为局部私有都能避免多线程问题,不过也加重了创建对象的负担 。在一般情况下,这样其实对性能影响比不是很明显的 。
2、使用同步:同步对象
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、使用
import java.text.DateFormat;import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.Date;public class ConcurrentDateUtil {private static ThreadLocal threadLocal = new ThreadLocal() {@Overrideprotected 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);}}
说明:使用 , 也是将共享变量变为独享,线程独享肯定能比方法独享在并发环境中能减少不少创建对象的开销 。如果对性能要求比较高的情况下,一般推荐使用这种方法 。
Java 8 中的解决办法
Java 8 提供了新的日期时间 API,其中包括用于日期时间格式化的 ,它与最大的区别在于: 是线程安全的,而并不是线程安全 。
如何使用:
解析日期
String dateStr= "2018年06月20日";DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");LocalDate date= LocalDate.parse(dateStr, formatter);
日期转换为字符串
LocalDateTime now = LocalDateTime.now();DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy年MM月dd日 hh:mm a");String nowStr = now .format(format);
由的静态方法 () 构建日期格式,和等一些表示日期或时间的类使用 parse 和方法把日期和字符串做转换 。
使用新的 API,整个转换过程都不需要考虑线程安全的问题 。
总结
是线程不安全的类,多线程环境下注意线程安全问题,如果是 Java 8 ,建议使用代替。
参考资料
相关文章
【SimpleDateFormat 如何安全的使用?】20 个案例教你在 Java 8 中如何处理日期和时间?