SimpleDateFormat是Java提供的一个格式化和解析日期的工具类,多线程共用一个SimpleDateFormat实例对日期进行解析或者格式化会导致程序出错。
问题复现
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args){
for (int i = 0;i<10;i++){
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(sdf.parse("2023-02-08 16:45:40"));
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
}).start();
}
}
运行代码后抛出NumberFormatException
异常。
(多运行几次或增加线程数,有利于复现该问题。)
解决问题
每次使用时都new一个SimpleDateFormat的实例
public static void main(String[] args){
for (int i = 0;i<10;i++){
new Thread(new Runnable() {
@Override
public void run() {
try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.parse("2023-02-08 16:45:40"));
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
}).start();
}
}
但是每次使用都需要new一个对象,并且使用后由于没有其他引用,又需要回收,开销会很大。
使用synchronized进行同步
public static void main(String[] args){
for (int i = 0;i<10;i++){
new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (sdf){
System.out.println(sdf.parse("2023-02-08 16:45:40"));
}
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
}).start();
}
}
进行同步意味着多个线程要竞争锁,在高并发场景下这会导致系统响应性能下降。
使用ThreadLocal(推荐)
private static ThreadLocal<SimpleDateFormat> safeSdf = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static void main(String[] args){
for (int i = 0;i<20;i++){
new Thread(() -> {
try {
System.out.println(safeSdf.get().parse("2023-02-08 16:45:40"));
} catch (ParseException e) {
throw new RuntimeException(e);
}finally {
//使用完毕后清除,避免内存泄露
safeSdf.remove();
}
}).start();
}
}
使用DateTimeFormatter
DateTimeFormatter 是线程安全的。
static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args){
LocalDateTime localDateTime = LocalDateTime.parse("2023-02-21 10:10:11",formatter);
log.info(JSONObject.toJSONString(localDateTime));
log.info(localDateTime.format(DateTimeFormatter.BASIC_ISO_DATE));
}
这种方法比使用 SimpleDateFormat 组合 ThreadLocal 代码更简洁,速度也大约要快 50%。
参考
《Java并发编程之美》
《Java8实战》