在日常开发中,java.text.DateFormat 应该算是使用频率比较高的一个工具类,经常会使用它 将 Date 对象转换成字符串日期,或者将字符串日期转化成 Date 对象。先来看一段眼熟的代码:

public abstract class DateUtils {
 
  private static final DateFormat dateFormatForDay = new SimpleDateFormat("yyyyMMdd");
   
  public static String formatForDay(Date date){
    return dateFormatForDay.format(date);
  }
}

类 DateUtils 的方法 formatForDay() 在多线程的情况下,可能会得不到想要的结果。给一段多线程并发访问 formatForDay() 方法的示例:

public class DateFormatExample {
   
  static final class RunThread implements Runnable{
     
    private int i;
     
    public RunThread(int i) {
      this.i = i;
    }
 
    @Override
    public void run() {
      int count = 0;
      while(true){   
        // create a date
        Calendar c = Calendar.getInstance();
        c.add(Calendar.DAY_OF_MONTH, -i);
        Date d = c.getTime();
        // expect string
        String origianlDate = new SimpleDateFormat("yyyyMMdd").format(d);
        // DateUtils format string
        String afterDate = DateUtils.formatForDay(d);
         
        if(!origianlDate.equals(afterDate)){
          System.out.println("RunThread["+i+"]["+count+"] origianlDate = "+origianlDate
              +", afterDate = "+afterDate);
          System.exit(0);
        }
        count++;
      }
    }
  }
   
  public static void main(String[] args) {
    for(int i=0; i<100; i++){
      new Thread(new RunThread(i)).start();
    }
  }

示例代码,创建了100个 RunThread 线程,每个线程根据 i 的值创建不同的日期,并将日期格式化成字符串,和原日期进行对比,若不相等,则打印退出。某次执行的结果见下:

RunThread[85][1] origianlDate = 20170128, afterDate = 20170122
RunThread[91][5] origianlDate = 20170122, afterDate = 20170222
RunThread[32][7] origianlDate = 20170322, afterDate = 20170114
RunThread[60][3] origianlDate = 20170222, afterDate = 20170202
RunThread[1][0] origianlDate = 20170422, afterDate = 20170401

从结果可以看出,格式化后的日期和传给 format() 的日期不一致。

原因分析

类 DateFormat 有个 Calendar 成员变量:

public abstract class DateFormat extends Format {
 
    protected Calendar calendar;
 
    // ... other code
 
}

调用 format() 方法,会调用实现类 SimpleDateFormat 的  format(Date date, StringBuffer toAppendTo, FieldDelegate delegate) 方法:

public class SimpleDateFormat extends DateFormat {
 
    //... other code
 
    // Called from Format after creating a FieldDelegate
    private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {
        // Convert input date to time field list
        calendar.setTime(date);
 
        //... other code
   }
 
}

每次调用 format() 方法,会将要格式化的日期设置到成员变量 calendar 中,然后再对其进行格式化,此类实现未进行线程同步,是非线程安全的。

当然,调用 parse() 方法,将 字符串转化成日期,也会有同样的非线程安全问题。

解决方案

  1. 每次进行格式化日期调用时,均 new 一个 SimpleDateFormat 对象;缺点是在高并发的情况下,就会频繁创建和销毁对旬,造成开销。
  2. 使用 synchronized 关键字 或 Lock 给静态方法加上同步;缺点是在高并发的情况下,所有的线程在此处会引起资源的竞争。
  3. 使用 ThreadLocal 对象创建静态 DateFormat 。这样在高并必情况下,有多少个线程,就会创建多少个 DateFormat 对象,既不会无限制创建、销毁对象,也不会引起对象的多线程竞争,如下:
public abstract class DateUtils {
 
  private static final ThreadLocal<DateFormat> dateFormatForDay = new ThreadLocal<DateFormat>();
   
  public static String formatForDay(Date date){
    if(dateFormatForDay.get() == null){
      dateFormatForDay.set(new SimpleDateFormat("yyyyMMdd"));
    }
    return dateFormatForDay.get().format(date);
  }
}

java.text.DateFormat 多线程并发问题的更多相关文章

  1. java.text.DateFormat 日期格式化

    一: java.text.DateFormat <%@ page language="java" contentType="text/html; charset=u ...

  2. 常用类一一时间处理相关类一一java.util.Tomezone(java.util.Calendar , java.util.Date , java.text.DateFormat)

    时间处理相关类 时间是一个一维的东东.所以,我们需要一把刻度尺来区表达和度量时间.在计算机世界,我们把1970 年 1 月 1 日 00:00:00定为基准时间,每个度量单位是毫秒(1秒的千分之一). ...

  3. java.text.DateFormat 线程不安全问题

    java.text下的 DateFormat 是线程不安全的: 建议1: 1.使用threadLocal包装DateFormat(太复杂,不推荐) 2.使用org.apache.commons.lan ...

  4. java 多线程:线程安全问题,示例DateFormat多线程执行冲突解决方案ThreadLocal、方法内变量

    SimpleDateFormat多线程中执行报错 java.lang.NumberFormatException: For input string: ""   import ja ...

  5. 【java】Date与String之间的转换及Calendar类:java.text.SimpleDateFormat、public Date parse(String source) throws ParseException和public final String format(Date date)

    package 日期日历类; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util. ...

  6. java.text.ParseException: Unparseable date: "2015-06-09 hh:56:19"

    1.错误描述 [DEBUG:]2015-06-09 16:56:19,520 [-------------------transcation start!--------------] java.te ...

  7. java.text.SimpleDateFormat使用介绍

    java.text.SimpleDateFormat的使用 java.lang.Object   |   +----java.text.Format           |           +-- ...

  8. java.text.ParseException: Failed to parse date ["未知']

    先把"未知"替换为"" 直接new 出来的Gson 对象是无法解析为""的Date属性的,需要通过GsonBuilder来进行创建 Gson ...

  9. java.text.ParseException: Unparseable date: &quot;2015-06-09 hh:56:19&quot;

    1.错误描写叙述 [DEBUG:]2015-06-09 16:56:19,520 [-------------------transcation start!--------------] java. ...

随机推荐

  1. PowerManager和PowerManager.WakeLock详解

    最近在做项目过程中,有一个LocalPush的需求,需要保持屏幕点亮一段时间,并且在这个时间里,启动Service来执行请求服务器的操作,拿到热点的数据.所以,就找了一下相关的android开源项目. ...

  2. USB OTG简介、与普通USB线的区别

    USB有三类接口A类接口                     -----------最常见的扁平接口,四芯  VCC   GND   D+   D- B类接口                    ...

  3. Unhandled event loop exception GC overhead limit exceeded

    1.错误描述 java.lang.OutOfMemoryError: GC overhead limit exceeded at java.util.zip.ZipFile.<init>( ...

  4. 芝麻HTTP:

    只要你的Scrapy Field字段名字和 数据库字段的名字 一样.那么恭喜你你就可以拷贝这段SQL拼接脚本.进行MySQL入库处理. 具体拼接代码如下: def process_item(self, ...

  5. pat1021-1030

    1021求树的直径网上一搜就有,但是我不太理解 只需要一共求两次的dfs的论调,好吧我收回这句话,好想脑补了下,第一次dfs有多个最长点,只需要搜一个就行QAQ.这么看来我写麻烦了 #include& ...

  6. Mac 常用快捷键

    可以按下组合键来实现通常需要鼠标.触控板或其他输入设备才能完成的操作. 要使用键盘快捷键,需按住一个或多个修饰键,同时按快捷键的最后一个键.例如,要使用快捷键 Command-C(拷贝),请按住 Co ...

  7. 初识Windows服务 C#

    1.新建Windows服务   2.切换到代码视图,拷入如下代码 该服务以10S的间隔创建 d:/1.txt 文件 using System; using System.Collections.Gen ...

  8. 一个小的tab切换插件

    1//使用 var t1=new Tab({ etype:'onmou',//默认点击触发,如果事件写错了,当作单击 autoplay:2000,//有时间值(按照事件自动播放)和false(不自动播 ...

  9. 深入理解JVM垃圾收集机制(JDK1.8)

    垃圾收集算法 标记-清除算法 最基础的收集算法是"标记-清除"(Mark-Sweep)算法,分两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象. 不足: ...

  10. vue项目架构

    一.工程说明: 1.代码git仓库地址:git@gitlab.*****.git. 2.目录结构: 1>.index.html 为build打包发布网页入口: 2>.lieda文件夹代码项 ...