java中SimpleDateFormat 的多線程安全問題
SimpleDateFormat 的多線程安全問題。這是生產(chǎn)環(huán)境中一個(gè)非常經(jīng)典且危險(xiǎn)的問題,因?yàn)樗赡懿粫?huì)立即導(dǎo)致程序崩潰,而是 silently(靜默地)產(chǎn)生錯(cuò)誤的數(shù)據(jù),極難排查。
問題根源:可變狀態(tài)與競(jìng)爭(zhēng)條件
SimpleDateFormat 不是線程安全的,其根本原因在于它內(nèi)部維護(hù)了可變的、共享的狀態(tài)(一個(gè) Calendar 對(duì)象),并且沒有使用同步機(jī)制來保護(hù)這個(gè)狀態(tài)。
- 內(nèi)部狀態(tài):當(dāng)你調(diào)用 parse 或 format 方法時(shí),SimpleDateFormat 會(huì)使用其內(nèi)部的 Calendar 對(duì)象來執(zhí)行計(jì)算。
- 競(jìng)爭(zhēng)條件:
- 線程 A 調(diào)用 parse("2023-10-25"),開始解析,將日期值設(shè)置到內(nèi)部的 Calendar 中。
- 在 線程 A 完成解析并返回結(jié)果之前,線程 B 也調(diào)用了 parse("2024-11-30"),并清空/覆蓋了內(nèi)部 Calendar 的狀態(tài)。
- 此時(shí),線程 A 繼續(xù)執(zhí)行,從被 線程 B 污染了的 Calendar 中讀取值,最終返回一個(gè)錯(cuò)誤的 Date 對(duì)象(可能是 “2024-10-25” 或其他混亂的結(jié)果)。
- 更糟的情況下,可能會(huì)直接拋出 NumberFormatException、ArrayIndexOutOfBoundsException 等異常。
問題復(fù)現(xiàn)示例
下面的代碼清晰地展示了這個(gè)問題:
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CountDownLatch;
public class SimpleDateFormatThreadSafetyDemo {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws InterruptedException {
int threadCount = 10;
CountDownLatch latch = new CountDownLatch(threadCount);
String dateString = "2023-10-25 15:30:00";
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
// 所有線程都嘗試解析同一個(gè)字符串
Date date = sdf.parse(dateString);
// 打印解析結(jié)果和當(dāng)前線程,如果線程不安全,結(jié)果會(huì)五花八門
System.out.println(Thread.currentThread().getName() + " - Parsed date: " + date);
} catch (ParseException e) {
System.out.println(Thread.currentThread().getName() + " - Parse failed: " + e.getMessage());
} catch (NumberFormatException e) {
// 多線程下可能拋出的其他異常
System.out.println(Thread.currentThread().getName() + " - NumberFormatException: " + e.getMessage());
} finally {
latch.countDown();
}
}).start();
}
latch.await(); // 等待所有線程結(jié)束
System.out.println("All threads finished.");
}
}
運(yùn)行結(jié)果可能如下(每次運(yùn)行都可能不同):
Thread-2 - Parsed date: Wed Oct 25 15:30:00 CST 2023 // 正確 Thread-4 - Parsed date: Mon Nov 30 15:30:00 CST 2026 // 完全錯(cuò)誤! Thread-0 - Parse failed: For input string: "" Thread-1 - NumberFormatException: multiple points Thread-3 - Parsed date: Wed Oct 25 15:30:00 CST 2023 // 正確 ...
你可以看到,在并發(fā)訪問下,出現(xiàn)了:
- 解析出完全錯(cuò)誤的日期。
- 拋出 ParseException。
- 拋出其他運(yùn)行時(shí)異常,如 NumberFormatException。
解決方案
有幾種常見的方法來解決這個(gè)多線程安全問題。
方案 1:局部變量(每次創(chuàng)建新實(shí)例)【推薦用于低并發(fā)】
最簡(jiǎn)單的方法是在每個(gè)需要使用的的方法內(nèi)部創(chuàng)建新的 SimpleDateFormat 實(shí)例。
public Date parseDate(String dateString) throws ParseException {
// 每次調(diào)用都創(chuàng)建一個(gè)新的 SimpleDateFormat,線程私有,絕對(duì)安全
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.parse(dateString);
}
優(yōu)點(diǎn):簡(jiǎn)單直觀,絕對(duì)線程安全。
缺點(diǎn):如果方法被高頻調(diào)用,會(huì)創(chuàng)建大量臨時(shí)對(duì)象,增加 GC 壓力,性能較差。
方案 2:使用synchronized加鎖
將共享的 SimpleDateFormat 實(shí)例的訪問用 synchronized 塊保護(hù)起來。
public class DateUtils {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
public static Date parse(String dateString) throws ParseException {
synchronized (sdf) { // 使用類對(duì)象或sdf對(duì)象作為鎖
return sdf.parse(dateString);
}
}
public static String format(Date date) {
synchronized (sdf) {
return sdf.format(date);
}
}
}
優(yōu)點(diǎn):避免了對(duì)象的頻繁創(chuàng)建,復(fù)用了實(shí)例。
缺點(diǎn):在高并發(fā)場(chǎng)景下,鎖競(jìng)爭(zhēng)會(huì)成為性能瓶頸。
方案 3:使用ThreadLocal【最佳推薦】
這是兼顧性能和線程安全的最佳方案。它為每個(gè)線程提供一份獨(dú)立的 SimpleDateFormat 實(shí)例副本,從而避免了競(jìng)爭(zhēng)。
public class ThreadSafeDateFormatter {
private static final ThreadLocal<SimpleDateFormat> threadLocalDateFormat =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static Date parse(String dateString) throws ParseException {
// get() 方法會(huì)返回當(dāng)前線程獨(dú)有的 SimpleDateFormat 實(shí)例
return threadLocalDateFormat.get().parse(dateString);
}
public static String format(Date date) {
return threadLocalDateFormat.get().format(date);
}
// 重要!如果使用線程池,在線程任務(wù)結(jié)束時(shí)最好清理 ThreadLocal,防止內(nèi)存泄漏
public static void remove() {
threadLocalDateFormat.remove();
}
}
優(yōu)點(diǎn):
- 線程安全,每個(gè)線程有自己的副本,無競(jìng)爭(zhēng)。
- 高性能,避免了頻繁創(chuàng)建實(shí)例和鎖競(jìng)爭(zhēng)。
缺點(diǎn): - 使用稍復(fù)雜。
- 需要注意內(nèi)存泄漏問題(特別是在使用線程池時(shí)),在使用完畢后調(diào)用
remove()方法。
方案 4:切換到 Java 8 的java.time包【終極方案】
從 Java 8 開始,引入了全新的日期時(shí)間 API (java.time 包)。這些類(如 LocalDateTime, DateTimeFormatter) 是 不可變且線程安全的。
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class Java8DateUtils {
// DateTimeFormatter 是線程安全的,可以放心定義為常量
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static LocalDateTime parse(String dateString) {
return LocalDateTime.parse(dateString, formatter); // 線程安全
}
public static String format(LocalDateTime dateTime) {
return formatter.format(dateTime); // 線程安全
}
}
這是現(xiàn)代 Java 開發(fā)的首選方案。
優(yōu)點(diǎn):
- 絕對(duì)線程安全。
- API 設(shè)計(jì)更清晰、更強(qiáng)大。
- 是 Java 官方的未來方向。
總結(jié)
| 方案 | 線程安全 | 性能 | 推薦度 |
|---|---|---|---|
| 局部變量 | 安全 | 差(對(duì)象創(chuàng)建開銷) | ??? (簡(jiǎn)單場(chǎng)景) |
| synchronized | 安全 | 中(有鎖競(jìng)爭(zhēng)) | ?? ( legacy code ) |
| ThreadLocal | 安全 | 高 | ???? (維護(hù)舊項(xiàng)目時(shí)) |
| Java 8 DateTimeFormatter | 安全 | 高 | ????? (新項(xiàng)目必選) |
最終建議:
- 如果是新項(xiàng)目,請(qǐng)毫不猶豫地使用 Java 8 的 java.time API。
- 如果必須維護(hù)使用 SimpleDateFormat 的舊代碼,請(qǐng)使用 ThreadLocal 方案來修復(fù)多線程問題。
到此這篇關(guān)于java中SimpleDateFormat 的多線程安全問題的文章就介紹到這了,更多相關(guān)java SimpleDateFormat多線程安全內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Springboot啟動(dòng)執(zhí)行特定代碼的方式匯總
這篇文章主要介紹了Springboot啟動(dòng)執(zhí)行特定代碼的幾種方式,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-12-12
springboot中請(qǐng)求路徑配置在配置文件中詳解
這篇文章主要介紹了springboot中請(qǐng)求路徑配置在配置文件中,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01
Java 使用POI生成帶聯(lián)動(dòng)下拉框的excel表格實(shí)例代碼
本文通過實(shí)例代碼給大家分享Java 使用POI生成帶聯(lián)動(dòng)下拉框的excel表格,代碼簡(jiǎn)單易懂,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友參考下吧2017-09-09
SpringBoot實(shí)現(xiàn)微服務(wù)通信的多種方式
微服務(wù)通信是指在分布式系統(tǒng)中,各個(gè)微服務(wù)之間進(jìn)行數(shù)據(jù)交互和通信的過程,今天我們將探討在Spring Boot中實(shí)現(xiàn)微服務(wù)通信的多種方式,文章通過代碼示例給大家介紹的非常詳細(xì),需要的朋友可以參考下2024-07-07
JAVA之讀取properties時(shí)路徑的注意問題
這篇文章主要介紹了JAVA之讀取properties時(shí)路徑的注意問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08
Java中的Semaphore信號(hào)量使用方法代碼實(shí)例
這篇文章主要介紹了Java中的Semaphore信號(hào)量使用方法代碼實(shí)例,Semaphore是一種基于計(jì)數(shù)的信號(hào)量,它可以設(shè)定一個(gè)閾值,基于此,多個(gè)線程競(jìng)爭(zhēng)獲取許可信號(hào),做自己的申請(qǐng)后歸還,超過閾值后,線程申請(qǐng)?jiān)S可信號(hào)將會(huì)被阻塞,需要的朋友可以參考下2023-11-11
Java使用pdfbox實(shí)現(xiàn)給pdf文件加圖片水印
有時(shí)候需要給pdf加水印,市面上工具都是收費(fèi)的要會(huì)員,還是自食其力吧;嘗試過 spire.pdf.free 那個(gè)超過10頁就不行了!所以本文還是使用了pdfbox,感興趣的可以了解一下2022-11-11
Java時(shí)間輪調(diào)度算法的代碼實(shí)現(xiàn)
時(shí)間輪是一種高效的定時(shí)調(diào)度算法,主要用于管理延時(shí)任務(wù)或周期性任務(wù),它通過一個(gè)環(huán)形數(shù)組(時(shí)間輪)和指針來實(shí)現(xiàn),將大量定時(shí)任務(wù)分?jǐn)偟焦潭ǖ臅r(shí)間槽中,極大地降低了時(shí)間復(fù)雜度和資源開銷,本文給大家介紹了Java時(shí)間輪調(diào)度算法的代碼實(shí)現(xiàn),需要的朋友可以參考下2025-03-03
Spring Bean實(shí)例化實(shí)現(xiàn)過程解析
這篇文章主要介紹了Spring Bean實(shí)例化實(shí)現(xiàn)過程解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-02-02

