【JAVA8新的时间与日期 API】- 传统时间格式化的线程安全问题
Java8之前的日期和时间API,存在一些问题,最重要的就是线程安全的问题。这些问题都在Java8中的日期和时间API中得到了解决,而且Java8中的日期和时间API更加强大。
传统时间格式化的线程安全问题
示例:
import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.concurrent.*; public class TestOldSimpleDateFormat { public static void main(String[] args) throws Exception { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); Callable<Date> task = new Callable<Date>() { @Override public Date call() throws Exception { return sdf.parse("2020-01-01"); } }; ExecutorService pool = Executors.newFixedThreadPool(10); List<Future<Date>> list = new ArrayList<>(); for (int i=0;i<10;i++){ Future<Date> future = pool.submit(task); list.add(future); } for (Future<Date> future : list){ System.out.println(future.get()); }
pool.shutdown(); } }
以上代码运行会报错:
报错缘由:取部分源码解释
/** * 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; } public Date parse(String text, ParsePosition pos) { // 省略上面诸多代码 Date parsedDate; CalendarBuilder calb = new CalendarBuilder(); try { //这里这个 calendar 对象是 SimpleDateFormat 类的父类 DateFormat 中的属性 : protected Calendar calendar; parsedDate = calb.establish(calendar).getTime();//这个 calb.establish(calendar) 方法中,这个方法中的主要步骤不是原子操作,并且会对 calendar 对象进行修改,所以在多线程环境下就会出现线程安全问题。 // 省略下面面诸多代码 } catch (IllegalArgumentException e) { //省略......................... return null; } return parsedDate; }
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; }
综上,我们可以看到 SimpleDateFormat 类中的parse 方法,调用了 CalendarBuilder 的 establish(calendar) 方法,并在方法中,对 calendar 对象进行了各种判断及修改,并且这些操作都不是原子操作或同步操作,而这个calendar 对象又是 SimpleDateFormat 的父类 DateFormat 的一个实例变量,所以,在多线程同时调用SimpleDateFormat 的 parse 方法的时候,就会出现线程安全问题。
针对以上异常,JAVA8之前的解决办法:
1. 将 SimpleDateFormat 对象定义成局部变量。
2. 加锁。
3. 使用ThreadLocal,每个线程都拥有自己的SimpleDateFormat对象副本。
示例(加锁):
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); Callable<Date> task = new Callable<Date>() { @Override public synchronized Date call() throws Exception {//加个同步,解决问题 return sdf.parse("2020-01-01"); // return DateFormatThreadLocal.convert("2020-01-01"); } }; ExecutorService pool = Executors.newFixedThreadPool(10); List<Future<Date>> list = new ArrayList<>(); for (int i=0;i<10;i++){ Future<Date> future = pool.submit(task); list.add(future); } for (Future<Date> future : list){ System.out.println(future.get()); } pool.shutdown();
示例(ThreadLocal):
import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; public class DateFormatThreadLocal { private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() { protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); } }; public static Date convert(String source) throws Exception { return df.get().parse(source); } } //////////////////////////////////////////////////////////////// public class TestOldSimpleDateFormat { public static void main(String[] args) throws Exception { // SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); Callable<Date> task = new Callable<Date>() { @Override public Date call() throws Exception { // return sdf.parse("2020-01-01"); return DateFormatThreadLocal.convert("2020-01-01"); } }; ExecutorService pool = Executors.newFixedThreadPool(10); List<Future<Date>> list = new ArrayList<>(); for (int i=0;i<10;i++){ Future<Date> future = pool.submit(task); list.add(future); } for (Future<Date> future : list){ System.out.println(future.get()); }
pool.shutdown();
} }
JAVA8的解决办法:使用新的API(DateTimeFormatter 和 LocalDate )
示例:
import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; import java.util.concurrent.*; public class TestOldSimpleDateFormat { public static void main(String[] args) throws Exception { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); Callable<LocalDate> task = new Callable<LocalDate>() { @Override public LocalDate call() throws Exception { // return sdf.parse("2020-01-01"); return LocalDate.parse("2020-01-01",formatter); } }; ExecutorService pool = Executors.newFixedThreadPool(10); List<Future<LocalDate>> list = new ArrayList<>(); for (int i=0;i<10;i++){ Future<LocalDate> future = pool.submit(task); list.add(future); } for (Future<LocalDate> future : list){ System.out.println(future.get()); } pool.shutdown(); } }