SimpleDateFormat是Java中非常常见的一个类,用来解析和格式化日期字符串。但是SimpleDateFormat在多线程的环境并不是安全的,这个是很容易犯错的部分,接下来讲一下这个问题出现的过程以及解决的思路。

问题描述:
先看代码,用来获取一个月的天数的:

  1. import java.text.SimpleDateFormat;
  2. import java.util.Calendar;
  3. import java.util.Date;
  4.  
  5. public class DateUtil {
  6.  
  7. /**
  8. * 获取月份天数
  9. * @param time 201202
  10. * @return
  11. */
  12. public static int getDays(String time) throws Exception {
  13. // String time = "201202";
  14. SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM");
  15. Date date = sdf.parse(time);
  16. Calendar c = Calendar.getInstance();
  17. c.setTime(date);
  18. int day = c.getActualMaximum(Calendar.DATE);
  19. return day;
  20. }
  21.  
  22. }

可以看到在这个方法里,每次要获取值的时候就先要创建一个SimpleDateFormat的实例,频繁调用这个方法的情况下很耗性能。为了避免大量实例的频繁创建和销毁,我们通常会使用单例模式或者静态变量进行改造,一般会这么改:

  1. import java.text.SimpleDateFormat;
  2. import java.util.Calendar;
  3. import java.util.Date;
  4.  
  5. public class DateUtil {
  6.  
  7. private static SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM");
  8.  
  9. /**
  10. * 获取月份天数
  11. * @param time 201202
  12. * @return
  13. */
  14. public static int getDays(String time) throws Exception {
  15. // String time = "201202";
  16. Date date = sdf.parse(time);
  17. Calendar c = Calendar.getInstance();
  18. c.setTime(date);
  19. int day = c.getActualMaximum(Calendar.DATE);
  20. return day;
  21. }
  22.  
  23. }

此时不管调用多少次这个方法,java虚拟机里只有一个SimpleDateFormat对象,效率和性能肯定要比第一个方法好,这个也是很多程序员选择的方法。但是,在这个多线程的条件下,多个thread共享同一个SimpleDateFormat,而SimpleDateFormat本身又是线程非安全的,这样就很容易出各种问题。

验证问题:
用一个简单的例子验证一下多线程环境下SimpleDateFormat的运行结果:

  1. import java.text.ParseException;
  2. import java.text.SimpleDateFormat;
  3. import java.util.Date;
  4. import java.util.concurrent.CountDownLatch;
  5.  
  6. public class DateUtil {
  7. private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  8.  
  9. public static String format(Date date) {
  10. return dateFormat.format(date);
  11. }
  12.  
  13. public static Date parse(String dateStr) throws ParseException {
  14. return dateFormat.parse(dateStr);
  15. }
  16.  
  17. public static void main(String[] args) {
  18. final CountDownLatch latch = new CountDownLatch(1);
  19. final String[] strs = new String[] {"2016-01-01 10:24:00", "2016-01-02 20:48:00", "2016-01-11 12:24:00"};
  20. for (int i = 0; i < 10; i++) {
  21. new Thread(new Runnable() {
  22. @Override
  23. public void run() {
  24. try {
  25. latch.await();
  26. } catch (InterruptedException e) {
  27. e.printStackTrace();
  28. }
  29.  
  30. for (int i = 0; i < 10; i++){
  31. try {
  32. System.out.println(Thread.currentThread().getName()+ "\t" + parse(strs[i % strs.length]));
  33. Thread.sleep(100);
  34. } catch (ParseException e) {
  35. e.printStackTrace();
  36. } catch (InterruptedException e) {
  37. e.printStackTrace();
  38. }
  39. }
  40. }
  41. }).start();
  42. }
  43. latch.countDown();
  44. }
  45. }

看一下运行的结果:

  1. Thread-9 Fri Jan 01 10:24:00 CST 2016
  2. Thread-1 Sat Feb 25 00:48:00 CST 20162017
  3. Thread-5 Sat Feb 25 00:48:00 CST 20162017
  4. Exception in thread "Thread-4" Exception in thread "Thread-6" java.lang.NumberFormatException: For input string: "2002.E20022E"
  5. at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
  6. at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
  7. at java.lang.Double.parseDouble(Double.java:538)
  8. at java.text.DigitList.getDouble(DigitList.java:169)
  9. at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
  10. at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
  11. at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
  12. at java.text.DateFormat.parse(DateFormat.java:364)
  13. at DateUtil.parse(DateUtil.java:24)
  14. at DateUtil$2.run(DateUtil.java:45)

那么为什么SimpleDateFormat不是线程安全的呢?

查找问题:

首先看一下SimpleDateFormat的源码:

  1. private StringBuffer format(Date date, StringBuffer toAppendTo,
  2. FieldDelegate delegate) {
  3. // Convert input date to time field list
  4. calendar.setTime(date);
  5.  
  6. boolean useDateFormatSymbols = useDateFormatSymbols();
  7.  
  8. for (int i = 0; i < compiledPattern.length; ) {
  9. int tag = compiledPattern[i] >>> 8;
  10. int count = compiledPattern[i++] & 0xff;
  11. if (count == 255) {
  12. count = compiledPattern[i++] << 16;
  13. count |= compiledPattern[i++];
  14. }
  15.  
  16. switch (tag) {
  17. case TAG_QUOTE_ASCII_CHAR:
  18. toAppendTo.append((char)count);
  19. break;
  20.  
  21. case TAG_QUOTE_CHARS:
  22. toAppendTo.append(compiledPattern, i, count);
  23. i += count;
  24. break;
  25.  
  26. default:
  27. subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
  28. break;
  29. }
  30. }
  31. return toAppendTo;
  32. }

可以看到format()方法先将日期存放到一个Calendar对象中,而这个Calendar在SimpleDateFormat中是以成员变量的形式存在的。随后调用subFormat()时会再次用到成员变量Calendar。这就是问题所在。同样,在parse()方法里也会存在相应的问题。
试想,在多线程环境下,如果两个线程都使用同一个SimpleDateFormat实例,那么就有可能存在其中一个线程修改了calendar后紧接着另一个线程也修改了calendar,那么随后第一个线程用到calendar时已经不是它所期待的值了。

避免问题:

那么,如何保证SimpleDateFormat的线程安全呢?
1.每次使用SimpleDateFormat时都创建一个局部的SimpleDateFormat对象,跟一开始的那个方法一样,但是存在性能上的问题,开销较大。
2.加锁或者同步

  1. import java.text.ParseException;
  2. import java.text.SimpleDateFormat;
  3. import java.util.Date;
  4.  
  5. public class DateSyncUtil {
  6.  
  7. private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  8.  
  9. public static String formatDate(Date date)throws ParseException{
  10. synchronized(sdf){
  11. return sdf.format(date);
  12. }
  13. }
  14.  
  15. public static Date parse(String strDate) throws ParseException{
  16. synchronized(sdf){
  17. return sdf.parse(strDate);
  18. }
  19. }
  20. }

当线程较多时,当一个线程调用该方法时,其他想要调用此方法的线程就要block,多线程并发量大的时候会对性能有一定的影响。
3.使用ThreadLocal

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

使用ThreadLocal可以确保每个线程都可以得到一个单独的SimpleDateFormat对象,既避免了频繁创建对象,也避免了多线程的竞争。

SimpleDateFormat,Calendar 线程非安全的问题的更多相关文章

  1. JDK中的SimpleDateFormat线程非安全

    在JDK中使用SimpleDateFormat的时候都会遇到线程安全的问题,在JDK文档中也说明了该类是线程非安全的,建议对于每个线程都创建一个SimpleDateFormat对象.如下面一个Case ...

  2. 如何在一个线程环境中使用一个线程非安全的java类

    在开发过程中 当我们拿到一个线程非安全的java类的时候,我们可以额外创建这个类的管理类 并在管理类中控制同步 比如 一个非线程安全的Pair类 package test.thread.sx.test ...

  3. 06_java 时间获取练习_Date\SimpleDateFormat\Calendar类练习

     1.获取当前的日期,并把这个日期转换为指定格式的字符串,如2088-08-08 08:08:08 import java.text.SimpleDateFormat; import java.uti ...

  4. 时间获取_Date\SimpleDateFormat\Calendar类

     1.获取当前的日期,并把这个日期转换为指定格式的字符串,如2088-08-08 08:08:08 import java.text.SimpleDateFormat; import java.uti ...

  5. java基础1.5版后新特性 自动装箱拆箱 Date SimpleDateFormat Calendar.getInstance()获得一个日历对象 抽象不要生成对象 get set add System.arrayCopy()用于集合等的扩容

    8种基本数据类型的8种包装类 byte Byte short Short int Integer long Long float Float double Double char Character ...

  6. Java基础 与时间日期相关的类:System -Date -SimpleDateFormat -Calendar类 -解决后缀.000Z 的时区问题

    笔记总结: /**与时间相关的类:System_Date_SimpleDateFormat_Calendar类 * 1.system 类下的currentTimeMillis() * 输出从1970年 ...

  7. session/SessionFactory线程非安全和线程安全

    SessionFactory负责创建session,SessionFactory是线程安全的,多个并发线程可以同时访问一个 SessionFactory 并从中获取Session实例. (Sessio ...

  8. ios专题 - 多线程非GCD(1)

    iOS多线程初体验是本文要介绍的内容,iPhone中的线程应用并不是无节制的,官方给出的资料显示iPhone OS下的主线程的堆栈大小是1M,第二个线程开始都是512KB.并且该值不能通过编译器开关或 ...

  9. 【源码】HashMap源码及线程非安全分析

    最近工作不是太忙,准备再读读一些源码,想来想去,还是先从JDK的源码读起吧,毕竟很久不去读了,很多东西都生疏了.当然,还是先从炙手可热的HashMap,每次读都会有一些收获.当然,JDK8对HashM ...

随机推荐

  1. 【HNOI2009】梦幻布丁

    题目描述 N个布丁摆成一行,进行M次操作.每次将某个颜色的布丁全部变成另一种颜色的,然后再询问当前一共有多少段颜色.例如颜色分别为1,2,2,1的四个布丁一共有3段颜色. 输入输出格式 输入格式 第一 ...

  2. php filesize() 方法返回的文件大小异常

    问题描述:需要生成一个 700k 大小左右的文件,相关代码如下: <?php $handle = fopen('./split.log', 'a+'); $result = 0; while( ...

  3. element-ui : <el-table> 按钮点击操作阻止@row-click

    描述:<el-table> 点击行时,会跳转到一个详细信息页面, 但是同时这一行也有编辑和删除按钮. 问题: 在点击按钮时,@row-click事件也被触发了,而我并不想触发 row-cl ...

  4. 【php】php与mysql初体验

    第一次体验在web站点上使用MySQL数据库,遇到了很多问题,总结如下: 1.安装XAMPP软件后,将文件放到hotdocs文件夹下,要访问其中的文件,使用localhost/XXX/XXX ,路径要 ...

  5. CentOS 安装jira 6.3.6

    java 目录: /usr/java/jdk1.6.0_45 tomcat 目录:/usr/tomcat-7.0.29 jira 目录: /usr/local/jira jira 访问地址: cent ...

  6. 关于IntelliJ IDEA 文档无法编辑的解决办法

    问题:在调试的时候,光标无法聚焦到代码区,导致无法编辑代码.停止调试后,问题仍然存在,需要重启idea. 这个问题纠结了我一个上午,百狗一通,发现都是说要卸载vim插件啥的,但是我是没装过vim插件. ...

  7. linux下搭建LAMP

    PHP命令找不到:  export PATH=$PATH:/usr/local/php/bin https://www.centos.bz/forum/thread-69-1-1.html 步骤: w ...

  8. HomeKit 开发指南(中文版)

    转载自cocoachina 本文由CocoaChina翻译组成员iBenjamin_Go和浅夏@旧时光翻译自苹果开发文档:HomeKit Developer Guide,敬请勘误. 本文档内容包括 第 ...

  9. DataGuard的三种保护模式

    (一)三种保护模式介绍1.最大性能模式这种模式保证数据库主库性能最大化,主备库之间数据是异步传输的.即,主备日志归档以后才会传输到备库,在备库上使用归档日志文件做恢复操作.这种模式提供在不影响prim ...

  10. oAuth2.0认证流程图

    这两天在看oAuth2.0的东西,简单的使用visio画了个流程图.演示的是用户登录慕课网,使用qq登录的流程: