還在用 SimpleDateFormat 做時間格式化?
點(diǎn)擊關(guān)注上方“Stephen”,
設(shè)為“置頂或星標(biāo)”,第一時間送達(dá)干貨
SimpleDateFormat.parse()方法的線程安全問題錯誤示例 非線程安全原因分析 解決方法 SimpleDateFormat.format()方法的線程安全問題錯誤示例 非線程安全原因分析 解決方法
SimpleDateFormat在多線程環(huán)境下存在線程安全問題。
1 SimpleDateFormat.parse() 方法的線程安全問題
1.1 錯誤示例
錯誤使用SimpleDateFormat.parse()的代碼如下:
import java.text.SimpleDateFormat;
public class SimpleDateFormatTest {
private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
/**
* SimpleDateFormat線程不安全,沒有保證線程安全(沒有加鎖)的情況下,禁止使用全局SimpleDateFormat,否則報錯 NumberFormatException
*
* private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
*/
for (int i = 0; i < 20; ++i) {
Thread thread = new Thread(() -> {
try {
// 錯誤寫法會導(dǎo)致線程安全問題
System.out.println(Thread.currentThread().getName() + "--" + SIMPLE_DATE_FORMAT.parse("2020-06-01 11:35:00"));
} catch (Exception e) {
e.printStackTrace();
}
}, "Thread-" + i);
thread.start();
}
}
}
報錯:

1.2 非線程安全原因分析
查看源碼中可以看到:SimpleDateFormat繼承DateFormat類,SimpleDateFormat轉(zhuǎn)換日期是通過繼承自DateFormat類的Calendar對象來操作的,Calendar對象會被用來進(jìn)行日期-時間計算,既被用于format方法也被用于parse方法。

SimpleDateFormat 的 parse(String source) 方法 會調(diào)用繼承自父類的 DateFormat 的 parse(String source) 方法

DateFormat 的 parse(String source) 方法會調(diào)用SimpleDateFormat中重寫的 parse(String text, ParsePosition pos) 方法,該方法中有個地方需要關(guān)注

SimpleDateFormat 中重寫的 parse(String text, ParsePosition pos) 方法中調(diào)用了 establish(calendar) 這個方法:

該方法中調(diào)用了 Calendar 的 clear() 方法

可以發(fā)現(xiàn)整個過程中Calendar對象它并不是線程安全的,如果,a線程將calendar清空了,calendar 就沒有新值了,恰好此時b線程剛好進(jìn)入到parse方法用到了calendar對象,那就會產(chǎn)生線程安全問題了!
正常情況下:

非線程安全的流程:

1.3 解決方法
方法1:每個線程都new一個SimpleDateFormat
import java.text.SimpleDateFormat;
public class SimpleDateFormatTest {
public static void main(String[] args) {
for (int i = 0; i < 20; ++i) {
Thread thread = new Thread(() -> {
try {
// 每個線程都new一個
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(Thread.currentThread().getName() + "--" + simpleDateFormat.parse("2020-06-01 11:35:00"));
} catch (Exception e) {
e.printStackTrace();
}
}, "Thread-" + i);
thread.start();
}
}
}
方式2:synchronized等方式加鎖
public class SimpleDateFormatTest {
private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
for (int i = 0; i < 20; ++i) {
Thread thread = new Thread(() -> {
try {
synchronized (SIMPLE_DATE_FORMAT) {
System.out.println(Thread.currentThread().getName() + "--" + SIMPLE_DATE_FORMAT.parse("2020-06-01 11:35:00"));
}
} catch (Exception e) {
e.printStackTrace();
}
}, "Thread-" + i);
thread.start();
}
}
}
方式3:使用ThreadLocal 為每個線程創(chuàng)建一個獨(dú)立變量
import java.text.DateFormat;
import java.text.SimpleDateFormat;
public class SimpleDateFormatTest {
private static final ThreadLocal<DateFormat> SAFE_SIMPLE_DATE_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static void main(String[] args) {
for (int i = 0; i < 20; ++i) {
Thread thread = new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "--" + SAFE_SIMPLE_DATE_FORMAT.get().parse("2020-06-01 11:35:00"));
} catch (Exception e) {
e.printStackTrace();
}
}, "Thread-" + i);
thread.start();
}
}
}
ThreadLocal的詳細(xì)使用細(xì)節(jié)見:
https://blog.csdn.net/QiuHaoqian/article/details/117077792
2 SimpleDateFormat.format() 方法的線程安全問題
2.1 錯誤示例
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class SimpleDateFormatTest {
// 時間格式化對象
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
public static void main(String[] args) throws InterruptedException {
// 創(chuàng)建線程池執(zhí)行任務(wù)
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
10, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
for (int i = 0; i < 1000; i++) {
int finalI = i;
// 執(zhí)行任務(wù)
threadPool.execute(new Runnable() {
@Override
public void run() {
Date date = new Date(finalI * 1000); // 得到時間對象
formatAndPrint(date); // 執(zhí)行時間格式化
}
});
}
threadPool.shutdown(); // 線程池執(zhí)行完任務(wù)之后關(guān)閉
}
/**
* 格式化并打印時間
*/
private static void formatAndPrint(Date date) {
String result = simpleDateFormat.format(date); // 執(zhí)行格式化
System.out.println("時間:" + result); // 打印最終結(jié)果
}
}

從上述結(jié)果可以看出,程序的打印結(jié)果竟然有重復(fù)內(nèi)容的,正確的情況應(yīng)該是沒有重復(fù)的時間才對。
2.2 非線程安全原因分析
為了找到問題所在,查看 SimpleDateFormat 中 format 方法的源碼來排查一下問題,format 源碼如下:

從上述源碼可以看出,在執(zhí)行 SimpleDateFormat.format() 方法時,會使用 calendar.setTime() 方法將輸入的時間進(jìn)行轉(zhuǎn)換,那么我們想想一下這樣的場景:
線程 1 執(zhí)行了 calendar.setTime(date)方法,將用戶輸入的時間轉(zhuǎn)換成了后面格式化時所需要的時間;線程 1 暫停執(zhí)行,線程 2 得到 CPU 時間片開始執(zhí)行; 線程 2 執(zhí)行了 calendar.setTime(date)方法,對時間進(jìn)行了修改;線程 2 暫停執(zhí)行,線程 1 得出 CPU 時間片繼續(xù)執(zhí)行,因為線程 1 和線程 2 使用的是同一對象,而時間已經(jīng)被線程 2 修改了,所以此時當(dāng)線程 1 繼續(xù)執(zhí)行的時候就會出現(xiàn)線程安全的問題了。
正常的情況下,程序的執(zhí)行是這樣的:

非線程安全的執(zhí)行流程是這樣的:

2.3 解決方法
同樣有三種解決方法
方法1:每個線程都new一個SimpleDateFormat
public class SimpleDateFormatTest {
public static void main(String[] args) throws InterruptedException {
// 創(chuàng)建線程池執(zhí)行任務(wù)
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
10, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
for (int i = 0; i < 1000; i++) {
int finalI = i;
// 執(zhí)行任務(wù)
threadPool.execute(new Runnable() {
@Override
public void run() {
// 得到時間對象
Date date = new Date(finalI * 1000);
// 執(zhí)行時間格式化
formatAndPrint(date);
}
});
}
// 線程池執(zhí)行完任務(wù)之后關(guān)閉
threadPool.shutdown();
}
/**
* 格式化并打印時間
*/
private static void formatAndPrint(Date date) {
String result = new SimpleDateFormat("mm:ss").format(date); // 執(zhí)行格式化
System.out.println("時間:" + result); // 打印最終結(jié)果
}
}
方式2:synchronized等方式加鎖
所有的線程必須排隊執(zhí)行某些業(yè)務(wù)才行,這樣無形中就降低了程序的運(yùn)行效率了
public class SimpleDateFormatTest {
// 時間格式化對象
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
public static void main(String[] args) throws InterruptedException {
// 創(chuàng)建線程池執(zhí)行任務(wù)
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
10, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
for (int i = 0; i < 1000; i++) {
int finalI = i;
// 執(zhí)行任務(wù)
threadPool.execute(new Runnable() {
@Override
public void run() {
Date date = new Date(finalI * 1000); // 得到時間對象
formatAndPrint(date); // 執(zhí)行時間格式化
}
});
}
// 線程池執(zhí)行完任務(wù)之后關(guān)閉
threadPool.shutdown();
}
/**
* 格式化并打印時間
*/
private static void formatAndPrint(Date date) {
// 執(zhí)行格式化
String result = null;
// 加鎖
synchronized (SimpleDateFormatTest.class) {
result = simpleDateFormat.format(date);
}
// 打印最終結(jié)果
System.out.println("時間:" + result);
}
}
方式3:使用ThreadLocal 為每個線程創(chuàng)建一個獨(dú)立變量
public class SimpleDateFormatTest {
// 創(chuàng)建 ThreadLocal 并設(shè)置默認(rèn)值
private static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss"));
public static void main(String[] args) {
// 創(chuàng)建線程池執(zhí)行任務(wù)
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
// 執(zhí)行任務(wù)
for (int i = 0; i < 1000; i++) {
int finalI = i;
// 執(zhí)行任務(wù)
threadPool.execute(() -> {
Date date = new Date(finalI * 1000); // 得到時間對象
formatAndPrint(date); // 執(zhí)行時間格式化
});
}
threadPool.shutdown(); // 線程池執(zhí)行完任務(wù)之后關(guān)閉
}
/**
* 格式化并打印時間
*/
private static void formatAndPrint(Date date) {
String result = dateFormatThreadLocal.get().format(date); // 執(zhí)行格式化
System.out.println("時間:" + result); // 打印最終結(jié)果
}
}
來源:blog.csdn.net/QiuHaoqian/article/
details/116594422
END
關(guān)注 Stephen,一起學(xué)習(xí),一起成長。
點(diǎn)“在看”支持下吧
點(diǎn) 閱讀原文 可優(yōu)惠充值話費(fèi),流量,視頻會員等。
