SimpleDateFormat的線程安全問題

SimpleDateFormat在進行日期格式轉換時用的不少,可是 DateFormat 和 SimpleDateFormat 類不都是線程安全的,在多線程環境下調用 format() 和 parse() 方法應該使用同步代碼來避免問題html

 * <p>
 * Date formats are not synchronized.
 * It is recommended to create separate format instances for each thread.
 * If multiple threads access a format concurrently, it must be synchronized
 * externally.
 *
 * @see          <a href="http://java.sun.com/docs/books/tutorial/i18n/format/simpleDateFormat.html">Java Tutorial</a>
 * @see          java.util.Calendar
 * @see          java.util.TimeZone
 * @see          DateFormat
 * @see          DateFormatSymbols
 * @author       Mark Davis, Chen-Lieh Huang, Alan Liu
 */
public class SimpleDateFormat extends DateFormat {
...................
}

上面的註釋已經說的很清楚,日期格式化操做不是同步的,建議爲每一個線程穿件單獨的實例java

 public static final String PATTEN = "yyyy-MM-dd hh:mm:ss";

    public static final SimpleDateFormat sdf = new SimpleDateFormat(PATTEN);

    public static final CountDownLatch countDownLatch = new CountDownLatch(100);

    @Test
    public void testJDKSimpleDateFormat() {
        //jdk的實現方式,會有線程安全的問題
        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println(sdf.parseObject("2013-05-24 06:02:20"));
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }

上面的測試方法在多線程下會出現以下異常:git

Exception in thread "Thread-1" Exception in thread "Thread-3" java.lang.NumberFormatException: For input string: ""
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Long.parseLong(Long.java:453)
	at java.lang.Long.parseLong(Long.java:483)
	at java.text.DigitList.getLong(DigitList.java:194)
	at java.text.DecimalFormat.parse(DecimalFormat.java:1316)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1793)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455)
	at java.text.DateFormat.parseObject(DateFormat.java:415)
	at java.text.Format.parseObject(Format.java:243)
	at com.smart.tools.SimpleDateFormatTest$1.run(SimpleDateFormatTest.java:32)
	at java.lang.Thread.run(Thread.java:745)
Exception in thread "Thread-8" java.lang.NumberFormatException: For input string: ""
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Long.parseLong(Long.java:453)
	at java.lang.Long.parseLong(Long.java:483)
	at java.text.DigitList.getLong(DigitList.java:194)
	at java.text.DecimalFormat.parse(DecimalFormat.java:1316)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2088)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455)
	at java.text.DateFormat.parseObject(DateFormat.java:415)
	at java.text.Format.parseObject(Format.java:243)
	at com.smart.tools.SimpleDateFormatTest$1.run(SimpleDateFormatTest.java:32)
	at java.lang.Thread.run(Thread.java:745)
java.lang.NumberFormatException: For input string: ""
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Long.parseLong(Long.java:453)
	at java.lang.Long.parseLong(Long.java:483)
	at java.text.DigitList.getLong(DigitList.java:194)
	at java.text.DecimalFormat.parse(DecimalFormat.java:1316)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1793)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455)
	at java.text.DateFormat.parseObject(DateFormat.java:415)
	at java.text.Format.parseObject(Format.java:243)
	at com.smart.tools.SimpleDateFormatTest$1.run(SimpleDateFormatTest.java:32)
	at java.lang.Thread.run(Thread.java:745)
Fri May 24 06:02:20 CST 2013
Fri May 24 06:02:20 CST 2013
Fri May 24 06:02:20 CST 2013

SimpleDateFormat繼承了DateFormat,在DateFormat中定義了一個protected屬性的 Calendar類的對象:calendar。只是由於Calendar累的概念複雜,牽扯到時區與本地化等等,Jdk的實現中使用了成員變量來傳遞參數,這就形成在多線程的時候會出現錯誤。apache

  在format方法裏,有這樣一段代碼:api

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

calendar.setTime(date)這條語句改變了calendar,稍後,calendar還會用到(在subFormat方法裏),而這就是引起問題的根源。想象一下,在一個多線程環境下,有兩個線程持有了同一個SimpleDateFormat的實例,分別調用format方法:
  線程1調用format方法,改變了calendar這個字段。
  中斷來了。
  線程2開始執行,它也改變了calendar。
  又中斷了。
  線程1回來了,此時,calendar已然不是它所設的值,而是走上了線程2設計的道路。若是多個線程同時爭搶calendar對象,則會出現各類問題,時間不對,線程掛死等等。
  分析一下format的實現,咱們不難發現,用到成員變量calendar,惟一的好處,就是在調用subFormat時,少了一個參數,卻帶來了這許多的問題。其實,只要在這裏用一個局部變量,一路傳遞下去,全部問題都將迎刃而解。
  這個問題背後隱藏着一個更爲重要的問題--無狀態:無狀態方法的好處之一,就是它在各類環境下,均可以安全的調用。衡量一個方法是不是有狀態的,就看它是否改動了其它的東西,好比全局變量,好比實例的字段。format方法在運行過程當中改動了SimpleDateFormat的calendar字段,因此,它是有狀態的。安全

解決辦法多線程

一、每次須要的時候都建立一個新的實例maven

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

2.使用同步:同步SimpleDateFormat對象ide

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

三、使用common-lang中的api
測試

maven依賴:

<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-lang3</artifactId>
			<version>3.4</version>
		</dependency>
package com.smart.tools;

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

import org.apache.commons.lang3.time.FastDateFormat;
import org.junit.Test;

/**
 * 測試時間處理類的線程安全問題
 * @Description 
 * @author gaowenming
 */
public class SimpleDateFormatTest {

    public static final String PATTEN = "yyyy-MM-dd hh:mm:ss";

    public static final SimpleDateFormat sdf = new SimpleDateFormat(PATTEN);

    public static final CountDownLatch countDownLatch = new CountDownLatch(100);

    @Test
    public void testJDKSimpleDateFormat() {
        //jdk的實現方式,會有線程安全的問題
        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println(sdf.parseObject("2013-05-24 06:02:20"));
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }

    @Test
    public void testCommonLang() {
        //CommonLang第三方jar包實現
        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        FastDateFormat fdf = FastDateFormat.getInstance(PATTEN);
                        System.out.println(fdf.format(new Date()));
                        Date date = fdf.parse("2013-05-24 06:02:20");
                        System.out.println(date);
                        countDownLatch.countDown();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
        try {
            //等待100個線程都執行完
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^");
    }
}

上面用了CountDownLatch,爲了能觀察到線程中的打印結果。

相關文章
相關標籤/搜索