有时候我们会使用到JDK java.text.*
下的SimpleDateFormat
类来对我们的日期与字符串进行格式化得转换,此时我们很容易想到,要基于SimpleDateFormat
封装成一个工具类,笔者一开始的代码类似下面:
package com.zhegui.utils.date;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @author Zhegui
*
*/
public class MyDateUtil {
/**
* 默认格式化模式
*/
private static final String DEFAULT_FOMRT = "yyyy-MM-dd HH:mm:ss";
/**
* 全局sdf
*/
private static SimpleDateFormat sdf = new SimpleDateFormat();
/**
* 使用默认的 pattren
* @param date
* @return
*/
public static String format(Date date){
return format(date, DEFAULT_FOMRT);
}
/**
* 根据传入的pattern设置
* @param date
* @param pattern
* @return
*/
public static String format(Date date, String pattern){
if(pattern == null || "".equals(pattern)){
throw new IllegalArgumentException("pattern is not allow null or '' ");
}
return sdf.format(date);
}
/**
* 使用默认的pattern
* @param dateStr
* @return
*/
public static Date parse(String dateStr){
return parse(dateStr, DEFAULT_FOMRT);
}
/**
* 手动设置模式
* @param dateStr
* @param pattern
* @return
*/
public static Date parse(String dateStr, String pattern){
Date date = null;
try {
date = sdf.parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
}
怎么样,看起来相当简单,然而后来笔者了解到,实际上上面的代码是存在问题的。什么问题呢?那就是上面的代码会存在并发问题,原因在于SimpleDateFormat
不是线程安全的,而上面的写法,将SimpleDateFormat
作为static
,为全局共享。SimpleDateFormat
内部,使用成员变量calendar
存储我们的Date
(DateStr)信息,当我们调用parse
或者format
的时候,SimpleDateFormat
会使用成员变量calendar
(注意是成员变量)进行一些处理转换等操作,此时在高并发情况下,交替调用parse
或者format
就会到致calendar
内部信息不一样,从而得到错误的格式化结果。线程不安全更详细信息,读者可以参考这里。
为了解决这个问题,可以使用ThreadLocal,使得每个线程私有一个SimpleDateFormat,这样就不会出现线程安全问题了,代码如下:
package com.zhegui.utils.date;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 日期工具类
* 由于SimpleDateFormat不是线程安全的,使用ThreadLocal来封装,使得每个线程私有一个SimpleDateFormat
*
* 业务场景举例:
* 假设我们要做一个批量导入的功能,里面涉及到了dateFomrt的调用。
* 大约10万条数据,为了高效处理,我们开启10个线程处理
*
* 在这里由于SimpleDateFormat不是线程安全的,假设我们对于每条记录主动 new SimpleDateFormat(), 那么
* 就需要创建10万个SimpleDateFormat对象,而使用ThreadLocal,我们只需要创建10个个SimpleDateFormat对象
* 因为每个线程使用的是自己私有的SimpleDateFormat对象,因此是线程安全的
* @author Zhegui
*
*/
public class DateUtil {
/**
* 默认格式化模式
*/
private static final String DEFAULT_FOMRT = "yyyy-MM-dd HH:mm:ss";
/**
* 重写initialValue 方法
* 当我们使用
* ThreadLocal 的 get 方法的时候
* ThreadLocal 会先查看我们 是否主动使用了 set方法设置了值
* 如果没有,或者set进去的被remove了,就使用 initialValue 返回的值
* 如果不重写,initialValue 默认返回 null
*
* 这里重写了 initialValue 我们就不需要手动 set的方式去添加 SimpleDateFormat
*/
private static final ThreadLocal<SimpleDateFormat> threadLocalSdf = new ThreadLocal<SimpleDateFormat>(){
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat();
}
};
/**
* 设置格式化模式
* @param pattern
*/
public static void setPattern(String pattern){
threadLocalSdf.get().applyPattern(pattern);
}
/**
* 使用默认的 pattren
* @param date
* @return
*/
public static String format(Date date){
return format(date, DEFAULT_FOMRT);
}
/**
* 根据传入的pattern设置
* @param date
* @param pattern
* @return
*/
public static String format(Date date, String pattern){
if(pattern == null || "".equals(pattern)){
throw new IllegalArgumentException("pattern is not allow null or '' ");
}
setPattern(pattern);
return threadLocalSdf.get().format(date);
}
/**
* 使用默认的pattern
* @param dateStr
* @return
*/
public static Date parse(String dateStr){
return parse(dateStr, DEFAULT_FOMRT);
}
/**
* 手动设置模式
* @param dateStr
* @param pattern
* @return
*/
public static Date parse(String dateStr, String pattern){
setPattern(pattern);
Date date = null;
try {
date = threadLocalSdf.get().parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
}
那么到这里有个问题,如果仅仅是为了保证线程安全,完全可以在第一个例子中,不将SimpleDateFormat
暴露为static
,每次使用parse
或者format
的时候new
一个SimpleDateFormat
,这样也是线程安全的,不一定要使用ThreadLocal
呢,其中又有什么不同呢?其实也是为了在高并发中提高效率,假设有这么一个场景:
假设我们要做一个批量导入的功能,里面涉及到了DateUtil
格式化的调用。大约10万条数据,为了高效处理,我们开启10个线程处理。假设我们对于每条记录的DateUtil
的使用的重新new SimpleDateFormat()
, 那么就需要创建10万个甚至更多的SimpleDateFormat
对象,而使用ThreadLocal
,我们只需要创建10个个SimpleDateFormat
对象,大大减少系统对SimpleDateFormat
对象的创建和销毁的消耗。