SimpleDateFormat,Calendar 線程非安全的問題


SimpleDateFormat是Java中非常常見的一個類,用來解析和格式化日期字符串。但是SimpleDateFormat在多線程的環境並不是安全的,這個是很容易犯錯的部分,接下來講一下這個問題出現的過程以及解決的思路。

問題描述:
先看代碼,用來獲取一個月的天數的:

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

public class DateUtil {

    /**
     * 獲取月份天數
     * @param time 201202
     * @return
     */
    public static int getDays(String time) throws Exception {
//        String time = "201202";
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM");
        Date date = sdf.parse(time);
        Calendar c = Calendar.getInstance();
        c.setTime(date);
        int day = c.getActualMaximum(Calendar.DATE);
        return day;
    }
    
}

可以看到在這個方法里,每次要獲取值的時候就先要創建一個SimpleDateFormat的實例,頻繁調用這個方法的情況下很耗性能。為了避免大量實例的頻繁創建和銷毀,我們通常會使用單例模式或者靜態變量進行改造,一般會這么改:

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

public class DateUtil {

        private static SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM");

    /**
     * 獲取月份天數
     * @param time 201202
     * @return
     */
    public static int getDays(String time) throws Exception {
//        String time = "201202";        
        Date date = sdf.parse(time);
        Calendar c = Calendar.getInstance();
        c.setTime(date);
        int day = c.getActualMaximum(Calendar.DATE);
        return day;
    }
    
}

此時不管調用多少次這個方法,java虛擬機里只有一個SimpleDateFormat對象,效率和性能肯定要比第一個方法好,這個也是很多程序員選擇的方法。但是,在這個多線程的條件下,多個thread共享同一個SimpleDateFormat,而SimpleDateFormat本身又是線程非安全的,這樣就很容易出各種問題。

驗證問題:
用一個簡單的例子驗證一下多線程環境下SimpleDateFormat的運行結果:

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CountDownLatch;

public class DateUtil {
    private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static String format(Date date) {
        return dateFormat.format(date);
    }

    public static Date parse(String dateStr) throws ParseException {
        return dateFormat.parse(dateStr);
    }

    public static void main(String[] args) {
        final CountDownLatch latch = new CountDownLatch(1);
        final String[] strs = new String[] {"2016-01-01 10:24:00", "2016-01-02 20:48:00", "2016-01-11 12:24:00"};
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        latch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    for (int i = 0; i < 10; i++){
                        try {
                            System.out.println(Thread.currentThread().getName()+ "\t" + parse(strs[i % strs.length]));
                            Thread.sleep(100);
                        } catch (ParseException e) {
                            e.printStackTrace();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
        }
        latch.countDown();
    }
}

看一下運行的結果:

Thread-9    Fri Jan 01 10:24:00 CST 2016
Thread-1    Sat Feb 25 00:48:00 CST 20162017
Thread-5    Sat Feb 25 00:48:00 CST 20162017
Exception in thread "Thread-4" Exception in thread "Thread-6" java.lang.NumberFormatException: For input string: "2002.E20022E"
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
    at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
    at java.lang.Double.parseDouble(Double.java:538)
    at java.text.DigitList.getDouble(DigitList.java:169)
    at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    at java.text.DateFormat.parse(DateFormat.java:364)
    at DateUtil.parse(DateUtil.java:24)
    at DateUtil$2.run(DateUtil.java:45)

那么為什么SimpleDateFormat不是線程安全的呢?

 

查找問題:

首先看一下SimpleDateFormat的源碼:

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;
}

可以看到format()方法先將日期存放到一個Calendar對象中,而這個Calendar在SimpleDateFormat中是以成員變量的形式存在的。隨后調用subFormat()時會再次用到成員變量Calendar。這就是問題所在。同樣,在parse()方法里也會存在相應的問題。
試想,在多線程環境下,如果兩個線程都使用同一個SimpleDateFormat實例,那么就有可能存在其中一個線程修改了calendar后緊接着另一個線程也修改了calendar,那么隨后第一個線程用到calendar時已經不是它所期待的值了。

 

避免問題:

那么,如何保證SimpleDateFormat的線程安全呢?
1.每次使用SimpleDateFormat時都創建一個局部的SimpleDateFormat對象,跟一開始的那個方法一樣,但是存在性能上的問題,開銷較大。
2.加鎖或者同步

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class DateSyncUtil {

    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
      
    public static String formatDate(Date date)throws ParseException{
        synchronized(sdf){
            return sdf.format(date);
        }  
    }
    
    public static Date parse(String strDate) throws ParseException{
        synchronized(sdf){
            return sdf.parse(strDate);
        }
    } 
}

當線程較多時,當一個線程調用該方法時,其他想要調用此方法的線程就要block,多線程並發量大的時候會對性能有一定的影響。
3.使用ThreadLocal

public class DateUtil {
    private static ThreadLocal<SimpleDateFormat> local = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static String format(Date date) {
        return local.get().format(date);
    }

    public static Date parse(String dateStr) throws ParseException {
        return local.get().parse(dateStr);
    }
}

使用ThreadLocal可以確保每個線程都可以得到一個單獨的SimpleDateFormat對象,既避免了頻繁創建對象,也避免了多線程的競爭。

 


免責聲明!

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



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