SimpleDateFormat线程安全问题

前言

最近在查资料的时候,偶然看到了SimpleDateFormat不是线程安全的类的相关资料,说实话,一开始看的时候还是惊讶了一把的,从来没想过这个类居然不是线程安全的。今天就来看看这个类的线程安全问题。

SimpleDateFormat线程安全分析

先看一个很简单的日期处理工具类

public class DateUtil {

    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static String formatDate(Date date) throws ParseException {
        return sdf.format(date);
    }

    public static Date parse(String strDate) throws ParseException {

        return sdf.parse(strDate);
    }
}

看上去没有任何问题,我们测试一下。

package com.wangcc.springbootexample.dateformat;

import java.text.ParseException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import com.wangcc.springbootexample.utils.DateUtil;

/**
 * @ClassName: DateFormatTest
 * @Description: TODO
 * @author BryantCong
 * @date 2018年12月23日 下午11:37:13
 * 
 */
public class DateFormatTest {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService es = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 30; i++) {
            es.submit(() -> {

                try {
                    System.out.println(Thread.currentThread().getName() + ":" + DateUtil.parse("2018-12-23 06:02:20"));
                } catch (ParseException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }

            });
        }

        Thread.sleep(2000L);

        es.shutdown();

        if (es.awaitTermination(10, TimeUnit.SECONDS)) {
            System.out.println("线程池已关闭");
        } else {
            System.out.println("线程池未正常关闭");
        }
    }
}

pool-1-thread-1:Sat Apr 12 18:22:00 CST 2228
pool-1-thread-2:Sun Dec 23 06:02:20 CST 2018
pool-1-thread-3:Sat Apr 12 18:22:00 CST 2228
pool-1-thread-3:Sun Dec 23 06:02:20 CST 2018
pool-1-thread-1:Sun Dec 23 06:02:20 CST 2018
pool-1-thread-3:Sat Dec 23 06:02:20 CST 2017
pool-1-thread-2:Sun Dec 23 06:20:20 CST 2018
pool-1-thread-3:Sun Dec 23 06:02:20 CST 2018
pool-1-thread-1:Sun Dec 23 06:02:20 CST 2018
pool-1-thread-1:Wed Dec 23 06:02:20 CST 223180618
pool-1-thread-1:Sun Dec 23 06:02:20 CST 2018
pool-1-thread-2:Sun Dec 23 06:02:20 CST 2018
pool-1-thread-1:Sat Dec 23 06:02:20 CST 2017
pool-1-thread-2:Sun Oct 08 19:00:43 CST 2023
pool-1-thread-1:Sun Feb 08 06:02:20 CST 42026
pool-1-thread-3:Sun Feb 08 06:02:20 CST 1
线程池已关闭

发现并发的时候会有问题,并不是每次输出的时间都是我们预期的,而且大多都不是我们预期的时间。我们在设计日期工具类的时候,复用了SimpleDateFormat对象,而且我们想当然的以为调用他的parse()方法和format()方法是线程安全的,其实,真的只是我们相当然而已,这两个方法都不是线程安全的,正因为不是线程安全的,所以这两个方法才不是静态方法,否则这两个方法直接是静态方法就好了(要保证线程安全,代表方法不会改变该类的状态)。

在SimpleDateFormat转换日期是通过Calendar对象来操作的,SimpleDateFormat继承DateFormat类,DateFormat类中维护一个Calendar对象

public abstract class DateFormat extends Format {

    /**
     * The {@link Calendar} instance used for calculating the date-time fields
     * and the instant of time. This field is used for both formatting and
     * parsing.
     *
     * <p>Subclasses should initialize this field to a {@link Calendar}
     * appropriate for the {@link Locale} associated with this
     * <code>DateFormat</code>.
     * @serial
     */
    protected Calendar calendar;
//DateFormat
 public Date parse(String source) throws ParseException
    {
        ParsePosition pos = new ParsePosition(0);
        Date result = parse(source, pos);
        if (pos.index == 0)
            throw new ParseException("Unparseable date: \"" + source + "\"" ,
                pos.errorIndex);
        return result;
    }
//SimpleDateFormat
@Override
    public Date parse(String text, ParsePosition pos){
      
      //省略大段代码
try {
            parsedDate = calb.establish(calendar).getTime();
            // If the year value is ambiguous,
            // then the two-digit year == the default start year
            if (ambiguousYear[0]) {
                if (parsedDate.before(defaultCenturyStart)) {
                    parsedDate = calb.addYear(100).establish(calendar).getTime();
                }
            }
        }
 //省略大段代码

}

在parse方法的最后,会调用CalendarBuilder的establish方法,入参就是SimpleDateFormat维护的Calendar实例,在establish方法中会调用calendar的clear方法,如下:

  Calendar establish(Calendar cal) {
        boolean weekDate = isSet(WEEK_YEAR)
                            && field[WEEK_YEAR] > field[YEAR];
        if (weekDate && !cal.isWeekDateSupported()) {
            // Use YEAR instead
            if (!isSet(YEAR)) {
                set(YEAR, field[MAX_FIELD + WEEK_YEAR]);
            }
            weekDate = false;
        }
//导致线程安全问题
        cal.clear();
        // Set the fields from the min stamp to the max stamp so that
        // the field resolution works in the Calendar.
    //导致线程安全问题
        for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
            for (int index = 0; index <= maxFieldIndex; index++) {
                if (field[index] == stamp) {
                    cal.set(index, field[MAX_FIELD + index]);
                    break;
                }
            }
        }
        
        //省略大段代码
        }

可知SimpleDateFormat维护的用parse方法计算日期-时间的calendar被清空了,如果此时线程A将calendar清空且没有设置新值,线程B也进入parse方法用到了SimpleDateFormat对象中的calendar对象,会产生线程安全问题。接着的for循环设置calendar的属性时,也会出现线程安全问题,所以我们会看到输出的时间在并发情况下会变得各种各样。

解决方法:

public class ConcurrentDateUtil {
    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static Date parse(String dateStr) throws ParseException {
        return threadLocal.get().parse(dateStr);
    }

    public static String format(Date date) {
        return threadLocal.get().format(date);
    }
}

每个线程分配一个SimpleDateFormat对象,这样就不会出现线程安全问题。

问题:format方法会不会也有线程安全问题?

  ExecutorService es = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 30; i++) {
            es.submit(() -> {

                try {
                    System.out.println(Thread.currentThread().getName() + ":" + DateUtil.formatDate(new Date()));
                } catch (ParseException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }

            });
        }

        Thread.sleep(2000L);

        es.shutdown();

        if (es.awaitTermination(10, TimeUnit.SECONDS)) {
            System.out.println("线程池已关闭");
        } else {
            System.out.println("线程池未正常关闭");
        }

将上面的测试方法换成format
输出如下

pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-3:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-1:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-3:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-1:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-3:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-1:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-3:2018-12-24 14:48:43
pool-1-thread-3:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-1:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-3:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-1:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-3:2018-12-24 14:48:43

运行多次的结果都是一样的,看不出有任何异常。
我在网上看到有些人说format方法是线程安全的,因为多次执行这个测试都没有发现任何异常。
其实,format方法与parse方法一样,都是非线程安全的。
我们看源码

//SimpleDateFormat
  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(date);这一行代码是肯定会导致线程不安全的。
你设置好的时间随时有可能被另一个线程的date覆盖,导致最后的值与期望值是不一致。而上面的测试方法为什么不会报错,很简单,就算被另外的线程覆盖,也还是原来的值(毫秒级的改变,我们无法察觉到)。所以我们不能用上述的测试方法来测试。

正确的测试方法

  ExecutorService es = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 50; i++) {
            es.submit(() -> {


                try {
                    int random = new Random().nextInt();
                    Date date = new Date();
                    if (random % 2 == 0) {
                        date = ConcurrentDateUtil.parse("2018-02-19 06:02:20");

                    }
                    if (!ConcurrentDateUtil.format(date).equals(DateUtil.formatDate(date))) {
                        System.out.println("证明非线程安全");
                    } else {
                        System.out.println("====");
                    }
                    // System.out.println(Thread.currentThread().getName() + "同步:" + date +
                    // "format:" + DateUtil.formatDate(date));
                } catch (ParseException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }

            });
        }


        es.shutdown();

        if (es.awaitTermination(10, TimeUnit.SECONDS)) {
            System.out.println("线程池已关闭");
        } else {
            System.out.println("线程池未正常关闭");
        }

输出如下

证明非线程安全
====
====
====
证明非线程安全
====
====
====
====
====
====

证明在多线程的情况下,会出现date覆盖的情况,非线程安全。

思考

说实话,一直以来,我都以为SimpleDateFormat这个类的所有操作都是线程安全的,像这种JDK提供的工具类,真的从来没想过他存在线程安全问题,但是他却偏偏就是个非线程安全的类。作为一个程序员,一定不能想当然,实践是检验真理的唯一标准。
另一方面,今天分析的这个问题也告诉了我们编写代码的需要注意的一些地方。

  • 我们最初提供的DateUtil类,他并不是一个无状态的类,这是很可怕的,我们绝对不能允许我们的Util类是一个有状态的类,这是非常非常可怕的,在我们对一些全局变量或者实例属性进行操作的时候,一定要考虑到对他们的操作会不会带来线程安全的问题。
  • 对于util这种公用类,我们一定要对其做并发测试!一定要做并发测试!一定要做并发测试!保证代码的正确性。
  • 我们在设计一个类的时候,尽量把他设计为无状态的。在Spring中,默认的Bean模式也是单例的,这要求我们的Bean要是无状态的,否则就会存在线程安全问题。如果是有状态的Bean,那么Bean模式必须要改为原型模式。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值