阿里巴巴规范
【强制】SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static,必须加锁,或者使用 DateUtils 工具类。
正例:注意线程安全,使用 DateUtils。亦推荐如下处理:
private static final ThreadLocal<DateFormat> dateStyle = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); } };
说明:如果是 JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar,DateTimeFormatter 代替SimpleDateFormat, 官方给出的解释: simple beautiful strong immutable thread-safe。
非线程安全的 SimpleDateFormat
Synchronization
Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.
See Also: Java Tutorial , Calendar, TimeZone, DateFormat, DateFormatSymbols
SimpleDateFormat 中的日期格式不是同步的。推荐(建议)为每个线程创建独立的格式实例。如果多个线程同时访问一个格式,则它必须保持外部同步。
写时间工具类,一般写成静态的成员变量,此种写法的多线程下的危险性?
SimpleDateFormat
是 Java 中用于日期格式化和解析的一个类,它在多线程环境中确实存在线程安全问题。这是因为 SimpleDateFormat
内部使用了一个 Calendar
实例来存储解析或格式化的日期,而这个 Calendar
实例是可变的。当多个线程共享一个 SimpleDateFormat
实例并尝试同时对日期进行解析或格式化时,可能会导致日期被错误解析或格式化,因为一个线程对 Calendar
实例的修改可能会影响到其他线程的操作。
多线程环境下的危险性
在多线程环境下使用共享的 SimpleDateFormat
实例进行日期的解析或格式化操作时,可能会遇到以下问题:
- 数据不一致:多个线程可能会同时尝试修改内部的
Calendar
对象,导致返回的日期数据不一致或错误。 - 抛出异常:在解析或格式化日期时可能会因为线程竞争而抛出异常,如
NumberFormatException
或ArrayIndexOutOfBoundsException
。
为什么 SimpleDateFormat
是线程不安全的?
SimpleDateFormat
的线程不安全主要来自于它内部使用的日历对象(Calendar
)。当调用 format
或 parse
方法时,SimpleDateFormat
会使用这个日历对象来存储中间状态。如果多个线程共享同一个 SimpleDateFormat
实例,它们会相互干扰这个状态,导致错误的格式化或解析结果。
源码分析
让我们通过 SimpleDateFormat
的部分关键源码来深入了解其内部工作机制和线程安全问题。
public class SimpleDateFormat extends DateFormat {
// Calendar 实例,用于日期时间计算
private transient Calendar calendar;
// pattern 是用户定义的格式化模式,如 "yyyy-MM-dd HH:mm:ss"
private String pattern;
// 其他相关成员变量,如时区、区域设置等
...
}
public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition pos) {
// 使用内部的 calendar 对象
calendar.setTime(date);
// 真正的格式化逻辑,依赖于 pattern 和 calendar 的状态
...
return toAppendTo;
}
public Date parse(String source, ParsePosition pos) {
// 解析逻辑,使用内部的 calendar 对象来存储解析的结果
...
return calendar.getTime();
}
从上述代码片段可以看出,SimpleDateFormat
使用内部的 Calendar
对象来表示日期和时间,这个对象是可变的并且在 format
和 parse
方法中被修改。如果多个线程同时调用同一个实例的这些方法,就会因为对共享状态的并发修改而导致不可预测的结果。
解决方法
-
局部变量:将
SimpleDateFormat
的实例定义为局部变量,每个线程都使用自己的实例,这样就避免了共享实例的问题。缺点:每调用一次方法就会创建一个
SimpleDateFormat
对象,方法结束又要作为垃圾回收。public class DateUtils { public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); /** * 模拟并发环境下使用SimpleDateFormat的parse方法将字符串转换成Date对象 * @param stringDate * @return * @throws Exception */ public static Date parseDate(String stringDate)throws Exception { return sdf.parse(stringDate); } public static void main(String[] args) throws Exception { for (int i = 1; i <=30; i++) { new Thread(() -> { try { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); System.out.println(sdf.parse("2020-11-11 11:11:11")); sdf = null; } catch (Exception e) { e.printStackTrace(); } },String.valueOf(i)).start(); } } }
-
同步代码块:在使用共享的
SimpleDateFormat
实例时,对其访问进行同步处理,例如使用synchronized
关键字同步代码块。但这种方法会降低程序的执行效率。 -
使用
ThreadLocal
:ThreadLocal
提供了线程局部变量,每个线程访问该变量时都有自己的独立初始化副本,这样就可以安全地使用SimpleDateFormat
。private static final ThreadLocal<SimpleDateFormat> dateFormatHolder = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyyMMdd HHmm"); } }; public static DateFormat getDateFormatter() { return dateFormatHolder.get(); }
-
使用新的日期和时间 API:从 Java 8 开始,引入了新的日期和时间 API(位于
java.time
包中),如DateTimeFormatter
,它是不变的且线程安全的,推荐使用这些新的 API 来替代SimpleDateFormat
。DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd HHmm");
总的来说,推荐在多线程环境下避免使用共享的 SimpleDateFormat
实例,而是采用 ThreadLocal
或使用 Java 8 的新日期和时间 API 来保证线程安全。
如何写一个线程安全的 DateUtils
使用ThreadLocal
确保每个线程有自己的SimpleDateFormat
实例
ThreadLocal
为每个使用该变量的线程提供了独立的变量副本,所以每个线程都可以独立地改变自己的副本而不会影响其他线程。
public class DateUtils {
private static final ThreadLocal<SimpleDateFormat> dateFormatHolder = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static Date parse(String dateStr) throws ParseException {
return dateFormatHolder.get().parse(dateStr);
}
public static String format(Date date) {
return dateFormatHolder.get().format(date);
}
}
每次使用时创建新的实例
尽管这种方法在性能上不是最优的,但它简单且线程安全。
public class DateUtils {
public static Date parse(String dateStr) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.parse(dateStr);
}
public static String format(Date date) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(date);
}
}
使用java.time
包中的类(Java 8及以上)
从Java 8开始,引入了新的时间日期API,这些API是不可变的且天然线程安全的。如果你能使用Java 8或更高版本,建议使用这些新的API,而不是SimpleDateFormat
。
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.ZoneId;
import java.util.Date;
public class DateUtils {
private static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static Date parse(String dateStr) {
LocalDateTime localDateTime = LocalDateTime.parse(dateStr, dateTimeFormatter);
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
}
public static String format(Date date) {
LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
return dateTimeFormatter.format(localDateTime);
}
}
总结
- 如果你仍在使用
SimpleDateFormat
且必须在多线程环境下工作,那么使用ThreadLocal
是一个线程安全的解决方案。 - 对于新的项目或者可以使用Java 8及以上版本的情况,推荐使用
java.time
包中的类,因为它们设计为不可变且线程安全。