SimpleDateFormat线程不安全解析以及解决方案

SimpleDateFormat线程不安全解析以及解决方案-图1

我们都知道SimpleDateFormat是Java中用来日期格式化的一个类,可以将特定格式的字符转转化成日期,也可将日期转化成特定格式的字符串。比如

  • 将特定的字符串转换成日期
public static void main(String[] args) throws ParseException {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        Date format = simpleDateFormat.parse("2022-12-19 11:55:56");
        System.out.println(format);
    }

  • 将日期转化成特定格式的字符串
public static void main(String[] args) throws ParseException {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
//        Date format = simpleDateFormat.parse("2022-12-19 11:55:56");
        String format = simpleDateFormat.format(new Date());
        System.out.println(format);
    }


这种都是在单线程的情况下,这样子是没有问题的,但是如果多线程调用同一个SimpleDateFormat 对象就会出现安全问题。

线程安全问题演示(参考了冰河的文章)

在下面的代码中我们定义了SimpleDateFormat对象被使用的总次数以及同时使用的最大线程的数量。在里面我用到了Semaphore 信号量用来限流,也就是限制线程的最大数量是20个。同时用到了CountDownLatch ,用来保证所有的线程都执行完成之后主线程才能继续往下执行。

package com.dongmu;

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


public class SimpleDateFormatTest {
//    定义执行的总次数
    private static final int EXECUTE_COUNT = 2000;
//    同时运行的最大的线程数量
    private static final int THREAD_COUNT = 20;
//    SimpleDateFormat对象
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
    public static void main(String[] args) throws InterruptedException {
        final Semaphore semaphore = new Semaphore(THREAD_COUNT);
        final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0;i<EXECUTE_COUNT;i++){
            executorService.execute(()->{
                try {
                    semaphore.acquire();

                    try {
                        simpleDateFormat.parse("2022-12-19");

                    } catch (ParseException e) {
                        System.out.println("线程"+Thread.currentThread().getName()+"格式化日期失败");
                        throw new RuntimeException(e);
                    }catch (NumberFormatException e){
                        System.out.println("线程"+Thread.currentThread().getName()+"格式化日期失败");
                        e.printStackTrace();
                    }
                    semaphore.release();

                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                countDownLatch.countDown();
            });
        }

        countDownLatch.await();
        executorService.shutdown();
        System.out.println("所有线程格式化日期完成");

    }
}

可以看到执行的结果如下:

这里报出了一个异常class NumberFormatException,这就是由于多线程使用同一个SimpleDateFormat对象导致的。

具体的原因是我们在调用parse方法的时候

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

而上面的携带两个参数的parse方法的最后调用了

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

这个establish方法接收了一个参数calendar,而这个对象是一个SimpleDateFormat对象的遍历,多线程使用同一个这个对象,而在这个establish方法中对calendar进行了clear和set的操作。这就导致了calendar对象内部的数据在多线程的操作下混乱了,也就导致在进行数据格式化的时候出现了原本不应该出现的数字,也就导致了NumberFormatException

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

同样在format中也存在同样的问题,也可能出现异常。

解决方案

  • 第一种就是直接每次使用的时候都new一个新SimpleDateFormat的对象。
  • 第二种就是在使用的时候用synchronized修饰。
  • 第三种就是
//Lock对象
private static Lock lock = new ReentrantLock();
在simpleDateFormat.parse("2022-12-19");的前面使用lock.lock();然后加上finally语句块,在这个语句快钟进行lock.unlock();
注意这里一定要在finally语句块钟进行执行释放锁的操作,避免因为程序异常导致锁无法是释放的问题。
  • 第四种方案,使用ThreadLocal保证每一个线程都拥有自己的simpleDateFormat变量,这样就不会出现线程安全问题了。
//定义成员变量
 private static ThreadLocal threadLocal = new ThreadLocal(){
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    };
//然后将原来parse的部分该成
threadLocal.get().parse("2022-12-19");
  • 第五种方案,使用DateTimeFormatter类,这是Java8提供的新的如期时间Api中的类,这是个线程安全的类,可以在高并发环境下直接使用。
private static DateTimeFormatter dateTimeFormatter =DateTimeFormatter.ofPattern("yyyy-MM-dd");

dateTimeFormatter.parse("2022-12-19");
  • 第六种方式使用.joda-time方式,这种方式需要引入依赖,这里不做过多介绍,上面的方法已经够用了。
转载请说明出处 内容投诉内容投诉
南趣百科 » SimpleDateFormat线程不安全解析以及解决方案

南趣百科分享生活经验知识,是您实用的生活科普指南。

查看演示 官网购买