0%

FastDateFormat为什么线程安全

FastDateFormat 线程安全的底层原理:从 SimpleDateFormat 的缺陷说起

在 Java 日期处理中,SimpleDateFormat 因线程不安全常导致诡异的并发问题,而 Apache Commons Lang 库的 FastDateFormat 则以线程安全为核心设计目标。本文将深入对比两者的实现差异,解析 FastDateFormat 如何通过设计规避线程安全问题,以及其缓存机制带来的性能优势。

SimpleDateFormat 的线程不安全根源

SimpleDateFormat 是 Java 原生的日期格式化工具,但在多线程环境下使用同一个实例会导致数据错乱,其根本原因在于共享的 Calendar 成员变量

核心问题:共享状态的并发修改

SimpleDateFormat 内部维护了一个 Calendar 实例(成员变量),用于解析和格式化日期。在多线程场景下,多个线程会同时操作这个共享的 Calendar,导致以下问题:

1
2
3
4
5
6
7
8
9
10
11
12
// SimpleDateFormat 的关键成员变量  
protected Calendar calendar;

// 格式化方法的核心逻辑(简化版)
private StringBuffer format(Date date, StringBuffer toAppendTo) {
// 步骤1:将日期设置到共享的 calendar 中
calendar.setTime(date); // 线程A执行到此处,尚未完成格式化

// 步骤2:基于 calendar 格式化字符串
// 若此时线程B调用 setTime 修改了 calendar,线程A的结果会被污染
// ... 格式化逻辑 ...
}
  • 并发冲突:线程 A 在执行 calendar.setTime(dateA) 后,尚未完成格式化,线程 B 调用 calendar.setTime(dateB) 覆盖了 calendar 的状态,导致线程 A 最终格式化的是 dateB 的值。
  • 表现症状:格式化结果出现随机的日期错乱(如年份、月份错误),且难以复现(与线程调度时机相关)。

常见错误用法

开发者常将 SimpleDateFormat 定义为静态变量以复用,这会放大线程安全问题:

1
2
3
4
5
6
7
// 错误示例:静态的 SimpleDateFormat 实例在多线程中共享  
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

// 多线程调用时会出现并发问题
public static String format(Date date) {
return sdf.format(date);
}

FastDateFormat 的线程安全设计

FastDateFormat 针对 SimpleDateFormat 的缺陷进行了彻底重构,通过消除共享状态引入缓存机制实现线程安全,同时保证高效复用。

核心改进:消除共享的 Calendar

FastDateFormat 摒弃了成员变量 Calendar,改为在方法内部创建局部变量 Calendar,确保每个线程操作的是独立的实例,从根源上避免并发冲突。

1
2
3
4
5
6
7
// FastDateFormat 的 format 方法(简化版)  
public String format(final Date date) {
// 局部变量 Calendar:每个线程调用时创建,互不干扰
final Calendar c = newCalendar(); // 创建新的 Calendar 实例
c.setTime(date); // 仅当前线程操作此实例
return applyRulesToString(c); // 基于当前 Calendar 格式化
}
  • 线程隔离:局部变量 c 属于方法栈帧,每个线程调用 format 时都会创建独立的 Calendar,不存在多线程共享问题。

缓存机制:高效复用格式化实例

FastDateFormat 通过缓存机制减少重复创建实例的开销,同时保证相同配置的格式化器唯一。

缓存的 key 设计

缓存的 key 由三个参数组成,确保相同配置的 FastDateFormat 只会被创建一次:

  • pattern:日期格式(如 yyyy-MM-dd);
  • timeZone:时区(如 Asia/Shanghai);
  • locale:国际化配置(如 Locale.CHINA)。

这些参数被封装为 MultipartKey 对象,作为缓存的 key。

缓存的实现:ConcurrentHashMap

缓存底层使用 ConcurrentHashMap 存储 MultipartKeyFastDateFormat 的映射,支持并发安全的读写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// FastDateFormat 的缓存实现(简化版)  
private static final FormatCache<FastDateFormat> cache = new FormatCache<FastDateFormat>() {
@Override
protected FastDateFormat createInstance(String pattern, TimeZone timeZone, Locale locale) {
return new FastDateFormat(pattern, timeZone, locale);
}
};

// 获取实例的方法
public static FastDateFormat getInstance(String pattern) {
return cache.getInstance(pattern, null, null); // 从缓存获取或创建
}

// FormatCache 中的缓存逻辑
public F getInstance(String pattern, TimeZone timeZone, Locale locale) {
final MultipartKey key = new MultipartKey(pattern, timeZone, locale);
F format = cInstanceCache.get(key); // cInstanceCache 是 ConcurrentHashMap
if (format == null) {
format = createInstance(pattern, timeZone, locale);
// 原子操作:若 key 不存在则放入,避免并发创建
final F previous = cInstanceCache.putIfAbsent(key, format);
if (previous != null) {
format = previous; // 取已存在的实例
}
}
return format;
}
  • 并发安全ConcurrentHashMapputIfAbsent 方法保证了多线程下不会重复创建相同配置的 FastDateFormat 实例。
  • 高效复用:相同格式、时区和国际化配置的场景会复用同一个实例,减少对象创建开销。

其他线程安全细节

  • 无状态设计FastDateFormat 的实例本身不包含任何可变状态(所有字段均为 final),仅在方法调用时使用局部变量,确保实例可安全共享。
  • 格式化规则预编译FastDateFormat 在初始化时会预编译日期格式规则(如解析 yyyy-MM-dd 为内部处理逻辑),避免每次格式化时重复解析,提升性能的同时减少线程间干扰。

FastDateFormat 与 SimpleDateFormat 的对比

特性 SimpleDateFormat FastDateFormat
线程安全 不安全(共享 Calendar) 安全(局部变量 Calendar + 无状态)
缓存机制 基于 ConcurrentHashMap 缓存实例
性能 低(并发冲突 + 无缓存) 高(无冲突 + 实例复用)
功能 支持格式化和解析 仅支持格式化(解析需用 FastDateParser
依赖 JDK 原生 Apache Commons Lang 库

使用建议

  • 优先使用 FastDateFormat:在多线程环境(如 Spring MVC 控制器、线程池任务)中,替代 SimpleDateFormat 避免并发问题。

  • 正确获取实例:通过静态方法 FastDateFormat.getInstance(...) 获取实例,而非直接 new,以利用缓存提升性能:

1
2
3
4
5
6
// 正确用法:从缓存获取实例  
private static final FastDateFormat fdf = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss");

public String format(Date date) {
return fdf.format(date); // 线程安全
}
  • 解析日期的替代方案FastDateFormat 不支持日期解析,可使用同库的 FastDateParser(同样线程安全):
1
2
3
4
5
6
7
import org.apache.commons.lang3.time.FastDateParser;  

private static final FastDateParser fdp = FastDateParser.getInstance("yyyy-MM-dd");

public Date parse(String dateStr) throws ParseException {
return fdp.parse(dateStr); // 线程安全的解析
}

SimpleDateFormat的线程不安全

大家都知道SimpleDateFormat是线程不安全的

1
protected Calendar calendar;

SimpleDateFormat中的calendar是成员变量,同实例多个线程下会共享该calendar对象

而在进行格式化的时候可能会由于第一个线程还没有格式化完成,而第二个线程已经将时间修改了的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private StringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {
// 如果第一个线程设置了时间之后还没有格式化为字符串,此时第二个线程将时间覆盖掉,就会出现线程安全问题
calendar.setTime(date);

boolean useDateFormatSymbols = useDateFormatSymbols();

for (int i = 0; i < compiledPattern.length; ) {
int tag = compiledPattern[i] >>> 8;
int count = compiledPattern[i++] & 0xff;
if (count == 255) {
count = compiledPattern[i++] << 16;
count |= compiledPattern[i++];
}

switch (tag) {
case TAG_QUOTE_ASCII_CHAR:
toAppendTo.append((char)count);
break;

case TAG_QUOTE_CHARS:
toAppendTo.append(compiledPattern, i, count);
i += count;
break;

default:
subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
break;
}
}
return toAppendTo;
}

FastDateFormat如何处理的呢

那么FastDateFormat为什么是线程安全的呢?首先FastDateFormat是有一个缓存的,在进行实例化的时候是通过cache缓存来获取实例的

1
2
3
4
5
6
7
8
9
10
private static final FormatCache<FastDateFormat> cache= new FormatCache<FastDateFormat>() {
@Override
protected FastDateFormat createInstance(final String pattern, final TimeZone timeZone, final Locale locale) {
return new FastDateFormat(pattern, timeZone, locale);
}
};

public static FastDateFormat getInstance(final String pattern) {
return cache.getInstance(pattern, null, null);
}

将格式化格式、时区和国际化作为一个key存在了cInstanceCache中,cInstanceCache是一个ConcurrentHashMap,相当于相同的格式化格式、时区和国际化会使用同一个FastDateFormat实例

1
2
3
4
5
6
7
8
9
10
11
final MultipartKey key = new MultipartKey(pattern, timeZone, locale);
F format = cInstanceCache.get(key);
if (format == null) {
format = createInstance(pattern, timeZone, locale);
final F previousValue= cInstanceCache.putIfAbsent(key, format);
if (previousValue != null) {
// another thread snuck in and did the same work
// we should return the instance that is in ConcurrentMap
format= previousValue;
}
}

而在使用FastDateFormat进行格式化的时候,是在方法中定义的Calendar局部变量,是不会出现线程安全问题的

1
2
3
4
5
public String format(final Date date) {
final Calendar c = newCalendar();
c.setTime(date);
return applyRulesToString(c);
}

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10