SimpleDateFormat的线程安全问题及解决办法

SimpleDateFormat是Java中的日期转换类,面试中也会经常问到为什么有线程安全问题,最近真的在运维项目中遇到这个问题,可能之前并发量很少没有发生,最近频繁出现才发现系统中很多处使用都有问题,简单说明下。

测试代码:

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

public class SimpleDateFormatTest {
    //SimpleDateFormat对象
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> {
                try {
                    try {
                        simpleDateFormat.parse("2022-02-02");
                        System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期成功!!!");
                    } catch (ParseException e) {
                        System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败;ParseException");
                        e.printStackTrace();
                        System.exit(1);
                    } catch (NumberFormatException e) {
                        System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败;NumberFormatException");
                        e.printStackTrace();
                        System.exit(1);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
        executorService.shutdown();
    }
}
线程:pool-1-thread-1 格式化日期失败;NumberFormatException
线程:pool-1-thread-10 格式化日期成功!!!
线程:pool-1-thread-6 格式化日期失败;NumberFormatException
线程:pool-1-thread-2 格式化日期失败;NumberFormatException
线程:pool-1-thread-8 格式化日期失败;NumberFormatException
线程:pool-1-thread-5 格式化日期失败;NumberFormatException
线程:pool-1-thread-3 格式化日期失败;NumberFormatException
线程:pool-1-thread-4 格式化日期失败;NumberFormatException
线程:pool-1-thread-9 格式化日期成功!!!
线程:pool-1-thread-7 格式化日期成功!!!
java.lang.NumberFormatException: For input string: "20222022022222022E42022022222022E4"
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
	at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
	at java.lang.Double.parseDouble(Double.java:538)
	at java.text.DigitList.getDouble(DigitList.java:169)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at com.danjiu.runtime.thread.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:22)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: multiple points
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
	at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
	at java.lang.Double.parseDouble(Double.java:538)
	at java.text.DigitList.getDouble(DigitList.java:169)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)

Parse最终的调用方法在CalendarBuilder.establish()方法中,先后调用了cal.clear()与cal.set(),先清除cal对象中设置的值,再重新设置新的值。由于Calendar内部并没有线程安全机制,并且这两个操作也都不是原子性的,所以当多个线程同时操作一个SimpleDateFormat时就会引起cal的值混乱。简单说SimpleDateFormat类不是线程安全的根本原因,DateFormat类中的Calendar对象被多线程共享,而Calendar对象本身不支持线程安全。

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;
                }
            }
        }

        if (weekDate) {
            int weekOfYear = isSet(WEEK_OF_YEAR) ? field[MAX_FIELD + WEEK_OF_YEAR] : 1;
            int dayOfWeek = isSet(DAY_OF_WEEK) ?
                                field[MAX_FIELD + DAY_OF_WEEK] : cal.getFirstDayOfWeek();
            if (!isValidDayOfWeek(dayOfWeek) && cal.isLenient()) {
                if (dayOfWeek >= 8) {
                    dayOfWeek--;
                    weekOfYear += dayOfWeek / 7;
                    dayOfWeek = (dayOfWeek % 7) + 1;
                } else {
                    while (dayOfWeek <= 0) {
                        dayOfWeek += 7;
                        weekOfYear--;
                    }
                }
                dayOfWeek = toCalendarDayOfWeek(dayOfWeek);
            }
            cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek);
        }
        return cal;
    }

解决办法:

1.通过ThreadLocal解决SimpleDateFormat类的线程安全问题
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){
    @Override
    protected DateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd");
    }
};
2.使用joda-time的DateTimeFormatter类解决线程安全问题
private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd");
DateTime.parse("2020-01-01", dateTimeFormatter).toDate();
3.FastDateFormat是apache的commons-lang3包提供的,线程安全
FastDateFormat format = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss");  
System.out.println(format.format(new Date()));  
4.其它方法
局部变量;
synchronized/lock锁;
因为性能等原因不建议使用,推荐方法1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值