Java併發編程筆記之SimpleDateFormat源碼分析

SimpleDateFormat 是 Java 提供的一個格式化和解析日期的工具類,平常開發中應該常常會用到,可是因爲它是線程不安全的,多線程公用一個 SimpleDateFormat 實例對日期進行解析或者格式化會致使程序出錯,本節就討論下它爲什麼是線程不安全的,以及如何避免。java

爲了復現上面所說的不安全,咱們要用一個例子來突出這個不安全,例子以下:安全

package com.hjc;

import java.text.ParseException;
import java.text.SimpleDateFormat;

/**
 * Created by cong on 2018/7/12.
 */
public class SimpleDateFormatTest {

    //(1)建立單例實例
    static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        //(2)建立多個線程,並啓動
        for (int i = 0; i <10 ; ++i) {
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    try {//(3)使用單例日期實例解析文本
                        System.out.println(sdf.parse("2018-07-12 15:18:00"));
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.start();//(4)啓動線程
        }
    }

}

運行結果以下:多線程

代碼(1)建立了 SimpleDateFormat 的一個實例,代碼(2)建立 10 個線程,每一個線程都公用同一個 sdf 對象對文本日期進行解析,多運行幾回就會拋出 java.lang.NumberFormatException 異常,加大線程的個數有利於該問題復現。ide

 

爲何會出現這樣的問題呢?工具

那麼接下來咱們就要進入到SimpleDateFormat 源碼一探究竟,爲了便於分析首先查看 SimpleDateFormat 的類圖結構,類圖以下所示:ui

可知每一個 SimpleDateFormat 實例裏面有一個 Calendar 對象,到後面就會知道SimpleDateFormat 之因此是線程不安全的,其實就是由於 Calendar 是線程不安全的,後者之因此是線程不安全的是由於其中存放日期數據的變量都是線程不安全的,好比裏面的 fields,time 等。spa

接下來咱們要看看parse方法到底幹了些什麼事,源碼以下:線程

 public Date parse(String text, ParsePosition pos)
    {

        //(1)解析日期字符串放入CalendarBuilder的實例calb中,源碼很長,省略一部分,本身去看
        .....

        Date parsedDate;
        try {//(2)使用calb中解析好的日期數據設置calendar
            parsedDate = calb.establish(calendar).getTime();
            ...
        }

        catch (IllegalArgumentException e) {
           ...
            return null;
        }

        return parsedDate;
    }
Calendar establish(Calendar cal) {
   ...
   //(3)重置日期對象cal的屬性值
   cal.clear();
   //(4) 使用calb中中屬性設置cal
   ...
   //(5)返回設置好的cal對象
   return cal;
}

代碼(1)主要的做用是解析字符串日期並把解析好的數據放入了 CalendarBuilder 的實例 calb 中,CalendarBuilder 是一個建造者模式,用來存放後面須要的數據。code

代碼(3)重置 Calendar 對象裏面的屬性值,源碼以下:orm

public final void clear(){
       for (int i = 0; i < fields.length; ) {
           stamp[i] = fields[i] = 0; // UNSET == 0
           isSet[i++] = false;
       }
       areAllFieldsSet = areFieldsSet = false;
       isTimeSet = false;
}

代碼(4)使用 calb 中解析好的日期數據設置 cal 對象

代碼(5) 返回設置好的 cal 對象

 

從上面代碼能夠知道代碼(3)(4)(5)操做不是原子性操做,當多個線程調用 parse 方法時候好比線程 A 執行了代碼(3)(4)也就是設置好了 cal 對象,在執行代碼(5)前線程 B 執行了代碼(3)清空了 cal 對象,因爲多個線程使用的是一個 cal 對象,因此線程 A 執行代碼(5)返回的就多是被線程 B 清空後的對象,固然也有可能線程 B 執行了代碼(4)被線程 B 修改後的 cal 對象。從而致使程序錯誤。

 

那麼,讓咱們思考一個問題,如何解決SimpleDateFormat 的線程安全性問題呢?

  1.第一種方式:每次使用時候 new 一個 SimpleDateFormat 的實例,這樣能夠保證每一個實例使用本身的 Calendar 實例, 可是每次使用都須要 new 一個對象,而且使用後因爲沒有其它引用,就會須要被回收,開銷會很大。

  2.第二種方式:究其緣由是由於多線程下代碼(3)(4)(5)三個步驟不是一個原子性操做,那麼容易想到的是對其進行同步,讓(3)(4)(5)成爲原子操做,可使用 synchronized 進行同步,例子改造以下所示:

/**
 * Created by cong on 2018/7/12.
 */
public class SimpleDateFormatTest1 {

    //(1)建立單例實例
    static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        //(2)建立多個線程,並啓動
        for (int i = 0; i <10 ; ++i) {
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    try {// (3)使用單例日期實例解析文本
                        synchronized (sdf) {
                            System.out.println(sdf.parse("2018-07-12 15:18:00"));
                        }
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.start();//(4)啓動線程
        }
    }

}

運行結果以下:

 

  3.第三種方式:使用 ThreadLocal,這樣每一個線程只須要使用一個 SimpleDateFormat 實例相比第一種方式大大節省了對象的建立銷燬開銷,而且不須要對多個線程直接進行同步,使用 ThreadLocal 方式來保證線程安全,例子以下:

/**
 * Created by cong on 2018/7/12.
 */
public class SimpleDateFormatTest2 {
    // (1)建立threadlocal實例
    static ThreadLocal<DateFormat> safeSdf = new ThreadLocal<DateFormat>(){
        @Override
        protected SimpleDateFormat initialValue(){
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static void main(String[] args) {
        // (2)建立多個線程,並啓動
        for (int i = 0; i < 10; ++i) {
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    try {// (3)使用單例日期實例解析文本
                        System.out.println(safeSdf.get().parse("2018-07-12 15:18:00"));
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }finally {
                        //(4)使用完畢記得清除,避免內存泄露
                        safeSdf.remove();
                    }
                }
            });
            thread.start();// (4)啓動線程
        }
    }

}

運行結果以下:

代碼(1)建立了一個線程安全的 SimpleDateFormat 實例,代碼(3)在使用的時候首先使用 get() 方法獲取當前線程下 SimpleDateFormat 的實例,在第一次調用 ThreadLocal 的 get()方法適合會觸發其 initialValue 方法用來建立當前線程所須要的 SimpleDateFormat 對象。另外須要注意的是代碼(4)使用完畢線程變量後要記得進行清理,以免內存泄露。

相關文章
相關標籤/搜索