SimpleDateFormat 線程不安全及解決方案

SimpleDateFormat定義

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

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

官網同步建議

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

爲何線程不安全

 

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

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

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

//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的時間。也會致使數據誤差app

 

不安全示例:ide

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
...

能夠看到數據出現誤差spa

 

解決方案.net

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原理分析

相關文章
相關標籤/搜索