天天看點

SimpleDateFormat線程不安全的原因以及解決方案

阿裡巴巴java開發手冊強制要求:

5. 【強制】SimpleDateFormat 是線程不安全的類,一般不要定義為 static 變量,如果定義為 static,必須加鎖,或者使用 DateUtils 工具類。(org.apache.commons.lang3.time.DateUtils)

因為SimpleDateFormat 繼承DateFormat,DateFormat中定義了屬性calendar。SimpleDateFormat 裡邊parse(..)方法和format(..)方法都使用到了calendar,并且使用過程中,對于cal.clear(),cal.set*(..)都沒有添加同步鎖。是以SimpleDateFormat類的對象并不适合在多線程中共用。

如下所示:

SimpleDateFormat線程不安全的原因以及解決方案
SimpleDateFormat線程不安全的原因以及解決方案
SimpleDateFormat線程不安全的原因以及解決方案
SimpleDateFormat線程不安全的原因以及解決方案
SimpleDateFormat線程不安全的原因以及解決方案
SimpleDateFormat線程不安全的原因以及解決方案
SimpleDateFormat線程不安全的原因以及解決方案
SimpleDateFormat線程不安全的原因以及解決方案

多線程中使用SimpleDateFormat:

可以在每個線程中new一個自己的SimpleDateFormat對象。(對象無法複用,頻繁的new和回收)

如果想在多線程中_共用SimpleDateFormat的對象,必須加鎖。(synchronized導緻降低多線程的效率)

可以使用import org.apache.commons.lang3.time.DateUtils進行一些時間的計算。

可以使用import org.apache.commons.lang3.time.DateFormatUtils進行日期和字元串的轉換。

阿裡巴巴java開發手冊推薦使用ThreadLocal<DateFormat>

private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() { 
    @Override 
    protected DateFormat initialValue() { 
        return new SimpleDateFormat("yyyy-MM-dd"); 
    } 
};
           

後邊有代碼示例。

注意:

6. 【強制】必須回收自定義的 ThreadLocal 變量,尤其線上程池場景下,線程經常會被複用,

如果不清理自定義的 ThreadLocal 變量,可能會影響後續業務邏輯和造成記憶體洩露等問題。

(如果使用代理模式和工廠模式使用線程池的話)盡量在代理中使用 try-finally 塊進行回收。

正例:

objectThreadLocal.set(userInfo);

try {

// ...

} finally {

objectThreadLocal.remove();

}

如果通過new ThreadPoolExecutor(..)建立線程池的話,可以等線程執行完畢以後對ThreadLocal進行回收:threadLocal.remove();。或直接關閉線程池:threadPool.shutDown();

ThreadLocal<DateFormat>代碼示例:

public static void main(String[] args) {
        ThreadLocal<SimpleDateFormat> localDateFormat = new ThreadLocal<SimpleDateFormat>();
        // 定義一個線程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(4, 10, 10L, TimeUnit.SECONDS, 
                new LinkedBlockingQueue<Runnable>(20),
                Executors.defaultThreadFactory(), 
                new ThreadPoolExecutor.CallerRunsPolicy());
        // 線程計數器
        CountDownLatch main = new CountDownLatch(12);
        
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                SimpleDateFormat dateFormat = localDateFormat.get();
                if (null == dateFormat) {
                    dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                    localDateFormat.set(dateFormat);
                    System.out.println(name+" 線程建立一個新的SimpleDateFormat");
                } else {
                    System.out.println(name+" 線程複用上次使用過的SimpleDateFormat");
                }
                // 線程任務執行完畢,主線程計數器減1。
                main.countDown();
            }
        };
        // 開辟線程執行任務
        for (int i = 0; i < 12; i++) {
            threadPool.execute(runnable);
            if (i == 3 || i == 7) {
                for (int j = 1; j <= 2100000000; j++) {
                    if (j == 2100000000) {
                        System.out.println("=================================");
                    }
                }
            }
        }

        try {
            // 線程阻塞,等到線程計數器歸零後繼續執行。
            main.await();
            System.out.println("所有線程任務執行完畢,回收localDateFormat。");
            localDateFormat.remove();
            System.out.println("線程池不用了。關閉線程池。如果之前沒有對localDateFormat進行回收,線程池關閉後,線程裡的localDateFormat會被回收。");
            threadPool.shutdown();
        } catch (InterruptedException e) {
            // shutdown()會線上程任務都執行完畢後将線程池關閉。
            threadPool.shutdown();
        }
    }
           

運作結果:

pool-1-thread-1 線程建立一個新的SimpleDateFormat
pool-1-thread-3 線程建立一個新的SimpleDateFormat
pool-1-thread-4 線程建立一個新的SimpleDateFormat
pool-1-thread-2 線程建立一個新的SimpleDateFormat
=================================
pool-1-thread-1 線程複用上次使用過的SimpleDateFormat
pool-1-thread-3 線程複用上次使用過的SimpleDateFormat
pool-1-thread-4 線程複用上次使用過的SimpleDateFormat
pool-1-thread-1 線程複用上次使用過的SimpleDateFormat
=================================
pool-1-thread-3 線程複用上次使用過的SimpleDateFormat
pool-1-thread-2 線程複用上次使用過的SimpleDateFormat
pool-1-thread-4 線程複用上次使用過的SimpleDateFormat
pool-1-thread-3 線程複用上次使用過的SimpleDateFormat
所有線程任務執行完畢,回收localDateFormat。
線程池不用了。關閉線程池。如果之前沒有對localDateFormat進行回收,線程池關閉後,線程裡的localDateFormat會被回收。
           

如果是 JDK8 的應用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar, DateTimeFormatter 代替 SimpleDateFormat

推薦博文:DateTimeFormatter、LocalDateTime 的使用