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