java多線程之DateTimeFormatter和SimpleDateFormat

本文參考自:https://www.jianshu.com/p/b212afa16f1f

1.SimpleDateFormat爲什麼不是線程安全的?

如果我們把SimpleDateFormat定義成static成員變量,那麼多個thread之間會共享這個SimpleDateFormat對象, 所以Calendar對象也會共享。

public static SimpleDateFormat formater = new SimpleDateFormat(
            "yyyy-MM-dd HH:mm:ss");

System.out.println(formater.format(new Date())+" Exception made...");

DateFormat.java

  public final String format(Date date)
    {
        return format(date, new StringBuffer(),
                      DontCareFieldPosition.INSTANCE).toString();
    }

    public abstract StringBuffer format(Date date, StringBuffer toAppendTo,
                                        FieldPosition fieldPosition);

SimpleDateFormat.java

@Override
public StringBuffer format(Date date, StringBuffer toAppendTo,
                           FieldPosition pos)
{
//  如此輕易地使用內部變量,肯定不能線程安全
//  線程都對pos進行寫操作,必然會影響其他線程的讀操作
    pos.beginIndex = pos.endIndex = 0;
    return format(date, toAppendTo, pos.getFieldDelegate());
}

private StringBuffer format(Date date, StringBuffer toAppendTo,
                            FieldDelegate delegate) {
    // Convert input date to time field list
   // 這裏已經徹底毀壞線程的安全性
    calendar.setTime(date);

    boolean useDateFormatSymbols = useDateFormatSymbols();

    for (int i = 0; i < compiledPattern.length; ) {
        int tag = compiledPattern[i] >>> 8;
        int count = compiledPattern[i++] & 0xff;
        if (count == 255) {
            count = compiledPattern[i++] << 16;
            count |= compiledPattern[i++];
        }

        switch (tag) {
        case TAG_QUOTE_ASCII_CHAR:
            toAppendTo.append((char)count);
            break;

        case TAG_QUOTE_CHARS:
            toAppendTo.append(compiledPattern, i, count);
            i += count;
            break;

        default:
            subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
            break;
        }
    }
    return toAppendTo;
}

1.1 復現錯誤
代碼參考

public class DateFormatTest {
    private static SimpleDateFormat sdf = new SimpleDateFormat("dd-MMM-yyyy", Locale.US);
    private static String date[] = { "01-Jan-1999", "09-Jan-2000", "08-Jan-2001" , "07-Jan-2002" , "06-Jan-2003" , "05-Jan-2004" , "04-Jan-2005" , "03-Jan-2006" , "02-Jan-2007" };

public static void main(String[] args) {
    for (int i = 0; i < date.length; i++) {
        final int temp = i;
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while (true) {
                        String str1 = date[temp];
                        String str2 = sdf.format(sdf.parse(str1));
                        System.out.println(Thread.currentThread().getName() + ", " + str1 + "," + str2);
                        if(!str1.equals(str2)){
                            throw new RuntimeException(Thread.currentThread().getName()
                                    + ", Expected " + str1 + " but got " + str2);
                        }
                    }
                } catch (Exception e) {
                    throw new RuntimeException("parse failed", e);
                }
            }
        }).start();
    }
}

}

2.SimpleDateFormat線程不安全的解決方法

2.1 將SimpleDateFormat定義成局部變量:

SimpleDateFormat sdf = new SimpleDateFormat("dd-MMM-yyyy", Locale.US);
String str1 = "01-Jan-2010";
String str2 = sdf.format(sdf.parse(str1));

缺點:每調用一次方法就會創建一個SimpleDateFormat對象,方法結束又要作爲垃圾回收。
2.2 加一把線程同步鎖:synchronized(lock)

public class SyncDateFormatTest {
    private static SimpleDateFormat sdf = new SimpleDateFormat("dd-MMM-yyyy", Locale.US);
    private static String date[] = { "01-Jan-1999", "01-Jan-2000", "01-Jan-2001" };
 
    public static void main(String[] args) {
        for (int i = 0; i < date.length; i++) {
            final int temp = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        while (true) {
                            synchronized (sdf) {
                                String str1 = date[temp];
                                Date date = sdf.parse(str1);
                                String str2 = sdf.format(date);
                                System.out.println(Thread.currentThread().getName() + ", " + str1 + "," + str2);
                                if(!str1.equals(str2)){
                                    throw new RuntimeException(Thread.currentThread().getName() 
                                            + ", Expected " + str1 + " but got " + str2);
                                }
                            }
                        }
                    } catch (Exception e) {
                        throw new RuntimeException("parse failed", e);
                    }
                }
            }).start();
        }
    }
}

缺點:性能較差,每次都要等待鎖釋放後其他線程才能進入
2.3 使用ThreadLocal
每個線程都將擁有自己的SimpleDateFormat對象副本。

public class DateUtil {
    private static ThreadLocal<SimpleDateFormat> local = new ThreadLocal<SimpleDateFormat>();
 
    public static Date parse(String str) throws Exception {
        SimpleDateFormat sdf = local.get();
        if (sdf == null) {
            sdf = new SimpleDateFormat("dd-MMM-yyyy", Locale.US);
            local.set(sdf);
        }
        return sdf.parse(str);
    }
    
    public static String format(Date date) throws Exception {
        SimpleDateFormat sdf = local.get();
        if (sdf == null) {
            sdf = new SimpleDateFormat("dd-MMM-yyyy", Locale.US);
            local.set(sdf);
        }
        return sdf.format(date);
    }
}

public class ThreadLocalDateFormatTest {
    private static String date[] = { "01-Jan-1999", "01-Jan-2000", "01-Jan-2001" };
 
    public static void main(String[] args) {
        for (int i = 0; i < date.length; i++) {
            final int temp = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        while (true) {
                            String str1 = date[temp];
                            Date date = DateUtil.parse(str1);
                            String str2 = DateUtil.format(date);
                            System.out.println(str1 + "," + str2);
                            if(!str1.equals(str2)){
                                throw new RuntimeException(Thread.currentThread().getName() 
                                        + ", Expected " + str1 + " but got " + str2);
                            }
                        }
                    } catch (Exception e) {
                        throw new RuntimeException("parse failed", e);
                    }
                }
            }).start();
        }
    }
}

3.使用DateTimeFormatter代替SimpleDateFormat
代碼參考DateTimeFormatterTest.java
jdk1.8中新增了 LocalDate 與 LocalDateTime等類來解決日期處理方法,同時引入了一個新的類DateTimeFormatter來解決日期格式化問題。
LocalDateTime,DateTimeFormatter兩個類都沒有線程問題,只要你自己不把它們創建爲共享變量就沒有線程問題。
可以使用Instant代替 Date,LocalDateTime代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat。

使用DateTimeFormatter完成格式化

DateTimeFormatter[] formatters = new DateTimeFormatter[]{
        // 直接使用常量創建DateTimeFormatter格式器
        DateTimeFormatter.ISO_LOCAL_DATE,
        DateTimeFormatter.ISO_LOCAL_TIME,
        DateTimeFormatter.ISO_LOCAL_DATE_TIME,
        // 使用本地化的不同風格來創建DateTimeFormatter格式器
        DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL, FormatStyle.MEDIUM),
        DateTimeFormatter.ofLocalizedTime(FormatStyle.LONG),
        // 根據模式字符串來創建DateTimeFormatter格式器
        DateTimeFormatter.ofPattern("Gyyyy%%MMM%%dd HH:mm:ss")
};
LocalDateTime date = LocalDateTime.now();
// 依次使用不同的格式器對LocalDateTime進行格式化
for(int i = 0 ; i < formatters.length ; i++)
{
    // 下面兩行代碼的作用相同
    System.out.println(date.format(formatters[i]));
    System.out.println(formatters[i].format(date));
}

使用DateTimeFormatter解析字符串

// 定義一個任意格式的日期時間字符串
String str1 = "2014==04==12 01時06分09秒";
// 根據需要解析的日期、時間字符串定義解析所用的格式器
DateTimeFormatter fomatter1 = DateTimeFormatter
    .ofPattern("yyyy==MM==dd HH時mm分ss秒");
// 執行解析
LocalDateTime dt1 = LocalDateTime.parse(str1, fomatter1);
System.out.println(dt1); // 輸出 2014-04-12T01:06:09
// ---下面代碼再次解析另一個字符串---
String str2 = "2014$$$四月$$$13 20小時";
DateTimeFormatter fomatter2 = DateTimeFormatter
    .ofPattern("yyy$$$MMM$$$dd HH小時");
LocalDateTime dt2 = LocalDateTime.parse(str2, fomatter2);
System.out.println(dt2); // 輸出 2014-04-13T20:00
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章