JUC
从阿里 ThreadLocal 规范开始

阿里巴巴规范

【强制】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 对象,导致返回的日期数据不一致或错误。
  • 抛出异常:在解析或格式化日期时可能会因为线程竞争而抛出异常,如 NumberFormatExceptionArrayIndexOutOfBoundsException

为什么 SimpleDateFormat 是线程不安全的?

SimpleDateFormat 的线程不安全主要来自于它内部使用的日历对象(Calendar)。当调用 formatparse 方法时,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 对象来表示日期和时间,这个对象是可变的并且在 formatparse 方法中被修改。如果多个线程同时调用同一个实例的这些方法,就会因为对共享状态的并发修改而导致不可预测的结果。

解决方法

  • 局部变量:将 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 关键字同步代码块。但这种方法会降低程序的执行效率。

  • 使用 ThreadLocalThreadLocal 提供了线程局部变量,每个线程访问该变量时都有自己的独立初始化副本,这样就可以安全地使用 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包中的类,因为它们设计为不可变且线程安全。