我们都知道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方式,这种方式需要引入依赖,这里不做过多介绍,上面的方法已经够用了。