1. 程式人生 > 實用技巧 >【JAVA8新的時間與日期 API】- 傳統時間格式化的執行緒安全問題

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