为什么SimpleDateFormat线程不安全

44 阅读2分钟

在进行日期的格式处理时,我们一般习惯于使用Date,同时用simpleDateFormat进行格式化。通常我们会在类中定义一个全局静态终态(final static)的SimpleDateFormat对象对所有方法中可能用到的日期进行处理,但是在多个线程并发执行的情况下,这样是存在问题的。

暴露问题

如下的代码模拟了多线程情况下对日期的解析情况,在每个线程的最后,比较了解析之后的年份和传入的年份是否相同。

public class dateTest {
    private static final SimpleDateFormat format =
            new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    private static final Executor threadPool =
            new ThreadPoolExecutor(50,50, 30, TimeUnit.MILLISECONDS,
                    new ArrayBlockingQueue<Runnable>(10),
                    new ThreadFactory() {
                        private final AtomicInteger order = new AtomicInteger(0);
                        @Override
                        public Thread newThread(Runnable r) {
                            return new Thread(r, "thread-"+ order.getAndIncrement());
                        }
                    });
    public static void main(String[] args) {
        for (int year = 120; year < 180; year++) {
            Date date = new Date(year, 1, 1);
            threadPool.execute(() -> {
                String format1 = format.format(date);
                System.out.println(Thread.currentThread().getName() + ",actualYear=" + format1.split("-")[0] + ", expectYear="+ (date.getYear()+1900) + ", result"+
                        format1.split("-")[0].equals(String.valueOf(date.getYear()+1900)));
            });
        }
    }
}

运行结果:

在多线程的情况下,存在问题,存在解析后的时间和解析之前的时间年限对不上的问题。这是为什么呢?

分析原因

如下是SimpleDateFormater的format方法,对于上面的示例,simpleDateFormat对象全局只有一个,但是在第四行,simpleDateFormat中的calendar对象发生了变化,且在第27行的方法内部,calendar参与构建format之后的结果,也就是toAppendTo。

private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {
        // Convert input date to time field list
        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;
    }

示意图:

执行分析:

  • 线程一执行完calendar.setTime,失去时间片
  • 线程二执行完calandar.setTime,失去时间片
  • 线程一继续执行 ,此时解析出的时间和传入的时间发生了偏差

替代方法

使用Java8中的时间相关API。

日期时间 API 是 Java 8 版本的最大特性之一。 Java 从一开始就缺少一致的日期和时间方法,Java 8 的核心 API 增加了对日期,时间,时间戳,时区等相关内容的完整补充,相关的类放在java.time.*这个包下:

新的时间相关API具有以下的特点:

不可变性: 新的时间API中的所有类都是不可变的,适用于多线程环境

概念分明: 单独定义了日期localDate, 时间LocalTime,时器&时间LocalDateTime,时间戳Instant,时区,Duration,Period等类。

所以,针对上面的问题,使用新的API就可以迎刃而解了~

LocalDate date = LocalDate.of(2010, 1, 1);
date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd");

_

下期内容:Java8中的时间相关API实践,敬请期待!