SimpleDateFormat 線程不安全及解決方案


SimpleDateFormat定義

SimpleDateFormat 是一個以與語言環境有關的方式來格式化和解析日期的具體類。它允許進行格式化(日期 -> 文本)、解析(文本 -> 日期)和規范化。

SimpleDateFormat 使得可以選擇任何用戶定義的日期-時間格式的模式。但是,仍然建議通過 DateFormat 中的 getTimeInstance、getDateInstance 或 getDateTimeInstance 
來創建日期-時間格式器。每一個這樣的類方法都能夠返回一個以默認格式模式初始化的日期/時間格式器。可以根據需要使用 applyPattern 方法來修改格式模式。

官網同步建議

同步
日期格式是不同步的。建議為每個線程創建獨立的格式實例。如果多個線程同時訪問一個格式,則它必須是外部同步的。

為什么線程不安全

 

上圖中,SimpleDateFormat類中,有個對象calendar

calendar 
          DateFormat 使用 calendar 來生成實現日期和時間格式化所需的時間字段值。

當SimpleDateFormat用static申明,多個線程共享SimpleDateFormat對象是,也共享該對象的calendar對象。而當調用parse方法時,會clear所有日歷字段和值。當線程A正在調用parse,線程B調用clear,這樣解析后的數據就會出現偏差

//parse方法
@Override
public Date parse(String text, ParsePosition pos)
{
    try {
            parsedDate = calb.establish(calendar).getTime();
            ...
    }
}


//establish方法
Calendar establish(Calendar cal) {
  ...  
  //將此 Calendar 的所有日歷字段值和時間值(從歷元至現在的毫秒偏移量)設置成未定義  
  cal.clear();  
}

同樣 formart中也用到了calendar對象,將date設置到日歷的時間字段中

 private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {
        // Convert input date to time field list
        calendar.setTime(date);
        ...
}

當線程A調用setTime,而線程B也調用setTime,這時候線程A最后得到的時間是 最后線程B的時間。也會導致數據偏差

 

不安全示例:

public static void main(String[] args) throws InterruptedException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateStr = "1111-11-11 11:11:11";
        ExecutorService executorService = Executors.newFixedThreadPool(100);
        for(int i=0;i<100;i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        //多個線程操作同一個sdf對象
                        System.out.println(sdf.format(sdf.parse(dateStr)) + "---" + Thread.currentThread().getName());
                    } catch (ParseException e) {
                        System.out.println("--------------> error, " + e.getMessage());
                    }
                }
            });

        }
        executorService.shutdown();
    }                                                    

執行結果:

...
1111-11-11 11:11:11---pool-1-thread-69
0011-11-11 11:11:11---pool-1-thread-72
0011-11-11 11:11:11---pool-1-thread-71
1111-11-11 11:11:11---pool-1-thread-73
1111-11-11 11:11:11---pool-1-thread-75
1111-11-11 11:11:11---pool-1-thread-76
1111-11-11 11:11:11---pool-1-thread-89
1111-11-11 00:11:11---pool-1-thread-93
1111-11-11 11:11:11---pool-1-thread-96
...

可以看到數據出現偏差

 

解決方案

1.為每個實例創建一個單獨的SimpleDateFormat對象

public static void main(String[] args) throws InterruptedException {
        //SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateStr = "1111-11-11 11:11:11";
        ExecutorService executorService = Executors.newFixedThreadPool(100);
        for(int i=0;i<100;i++) {
            executorService.submit(new Runnable() {
                //為每個線程創建自己的sdf對象
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                @Override
                public void run() {
                    try {
                        System.out.println(sdf.format(sdf.parse(dateStr)) + "---" + Thread.currentThread().getName());
                    } catch (ParseException e) {
                        System.out.println("--------------> error, " + e.getMessage());
                    }
                }
            });

        }
        executorService.shutdown();
    }

 

  缺點:每次new一個實例,都會new一個format對象,虛擬機內存消耗大,垃圾回收頻繁

2.給靜態SimpleDateFormat對象加鎖,使用Lock或者synchronized修飾

public static void main(String[] args) throws InterruptedException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateStr = "1111-11-11 11:11:11";
        ExecutorService executorService = Executors.newFixedThreadPool(100);
        for(int i=0;i<100;i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                   //加同步鎖
 synchronized (sdf) {
                        try {
                            System.out.println(sdf.format(sdf.parse(dateStr)) + "---" + Thread.currentThread().getName());
                        } catch (ParseException e) {
                            System.out.println("--------------> error, " + e.getMessage());
                        }
                    }
                }
            });

        }
        executorService.shutdown();
    }

 

  缺點:性能差,其他線程要等待鎖釋放

3.使用ThreadLocal為每個線程創建一個SimpleDateFormat對象副本,有線程隔離性,各自的副本對象也不會被其他線程影響

public static void main(String[] args) throws InterruptedException {
        //SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        //初始化threadLocal並設置值
        ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>(){ @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } };
        String dateStr = "1111-11-11 11:11:11";
        ExecutorService executorService = Executors.newFixedThreadPool(100);
        for(int i=0;i<100;i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println(threadLocal.get().format(threadLocal.get().parse(dateStr)) + "---" + Thread.currentThread().getName());
                    } catch (ParseException e) {
                        System.out.println("--------------> error, " + e.getMessage());
                    }
                }
            });

        }
        executorService.shutdown();
        //清理threadLocal,生產環境不清理容易導致內存溢出
threadLocal.remove();
}

 

ThreadLocal原理分析

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM