### 背景
生产环境中,登录接口出现偶发性的异常,排查发现是获取当前时间的工具类抛出异常,以下为代码片段:
``````java
/**
* 时间工具类
*/
public class DateUtil {
Logger logger= LoggerFactory.getLogger(this.getClass());
private final static SimpleDateFormat shortSdf = new SimpleDateFormat("yyyy-MM-dd");
private final static SimpleDateFormat longSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private final static SimpleDateFormat sdfYMD = new SimpleDateFormat("yyyyMMdd");
private final static SimpleDateFormat sdfHM= new SimpleDateFormat("yyyyMMddHHmm");
private final static SimpleDateFormat sdf= new SimpleDateFormat("yyyy/MM/dd");
static SimpleDateFormat sdfYM = new SimpleDateFormat("yyyy-MM"); // 日期格式
……
/**
* 获取当前日期
*
* @return Date 返回类型
*/
public static Date getNowDate() {
Date date = null;
try {
date = shortSdf.parse(shortSdf.format(new Date()));
} catch (ParseException e) {
}
return date;
}
````````
其中getNowDate()偶尔抛出异常:``java.lang.NumberFormatException: For input string: ""``
### 原因分析
SimpleDateFormat类本身是线程不安全的,当把SimpleDateFormat定义为类变量时,多个线程同时调用format和parse方法时,SimpleDateFormat的成员变量``protected Calendar calendar``,会被多个线程同时访问或修改,导致线程安全问题。
format方法会调用:``calendar.setTime(date);``
parse方法会调用:CalendarBuilder类的establish方法,其中会调用``cal.clear();``清空calendar实例的值。
### 解决方案
``SimpleDateFormat`` 类确实是线程不安全的,如果将其定义为类变量并在多线程环境下使用,可能会导致错误。有几种方法可以解决这个问题:
1. **局部变量**:将 ``SimpleDateFormat`` 作为局部变量使用。这样每个线程都会创建一个新的对象,避免了线程安全问题。
``````java
public String formatDate(Date date) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.format(date);
}
``````
2. **ThreadLocal**:使用 ``ThreadLocal`` 可以让每个线程都拥有自己的 ``SimpleDateFormat`` 实例,从而避免线程安全问题。
``````java
private static final ThreadLocal<SimpleDateFormat> dateFormatter = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
public String formatDate(Date date) {
return dateFormatter.get().format(date);
}
``````
3. **同步块**:使用同步块(synchronized block)来确保一次只有一个线程可以访问 ``SimpleDateFormat``,但这种方法可能会降低性能。
``````java
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
public String formatDate(Date date) {
synchronized (sdf) {
return sdf.format(date);
}
}
``````
4. **使用 Java 8 的 DateTimeFormatter**:Java 8 引入了一套新的时间日期API,其中包括线程安全的 ``DateTimeFormatter`` 类。推荐升级到 Java 8 并使用这个类来解决线程安全问题。
``````java
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
private static final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
public String formatDate(LocalDateTime date) {
return dtf.format(date);
}
``````
以上四种方法都可以解决 ``SimpleDateFormat`` 的线程安全问题,可以根据项目需求和实际情况选择最适合的方法。
全能宝已使用方案四,对公共组件的DateUtil工具类进行统一改造。
### 引申
在 JDK 1.8 中,主要有以下几个可能引发线程安全问题的类:
1. **SimpleDateFormat**: 如之前讨论的,``SimpleDateFormat`` 类是非线程安全的。在处理日期和时间时,推荐使用 Java 8 的新日期时间 API(如 ``LocalDate``、``LocalTime``、``LocalDateTime`` 和 ``DateTimeFormatter``)。
2. **Calendar**: ``Calendar`` 类也是线程不安全的。Java 8 推出了新的日期时间 API 来替代它,包括 ``LocalDate``、``LocalTime`` 和 ``LocalDateTime`` 等。
3. **Random**: ``java.util.Random`` 类在多线程环境下可能存在竞争条件,导致随机数生成不符合预期。为了解决这个问题,可以使用 ``java.util.concurrent.ThreadLocalRandom`` 类,它提供了线程安全的随机数生成。
4. **DecimalFormat**: ``java.text.DecimalFormat`` 类同样是线程不安全的。如果需要在多线程环境中进行数字格式化,可以考虑使用 ``ThreadLocal<DecimalFormat>`` 或将其作为局部变量。
5. **StringBuilder**: ``java.lang.StringBuilder`` 是线程不安全的,在多线程环境下应该使用 ``java.lang.StringBuffer``。但请注意,``StringBuffer`` 的性能略低于 ``StringBuilder``,因此只有在确实需要共享缓冲区的情况下才应使用 ``StringBuffer``。
总之,在使用这些类时,需要注意线程安全性。在多线程环境下,可以考虑使用线程安全的替代方案。对于日期和时间处理,推荐使用 Java 8 的新日期时间 API。