wtorek, 28 września 2010

SimpleDateFormat i wielowątkowość

Nie wszyscy zdają sobie sprawę, że standardowa w Javie klasa SimpleDateFormat z pakietu java.text nie jest bezpieczna ze względu na wątki. W sumie mało kto zdaje sobie z tego sprawę.

Bardzo często można spotkać w projektach Javy EE podobne linijki kodu:

Date date = GlobalConst.DATE_FMT.parse(datestring);

To, że potrzebna jest nam kontrola wielowątkowości pokaże następujący prosty przykład:

public class SimpleDateFormatTest {

  static SimpleDateFormat df = new SimpleDateFormat("dd-mm-yyyy");
  static String testdata[] = {
    "01-01-1999", "14-02-2001", "31-12-2007"
  };

  public static void main(String[] args) {
    Runnable r[] = new Runnable[testdata.length];
    for (int i = 0; i < r.length; i++) {
      final int i2 = i;
      r[i] = new Runnable() {

        public void run() {
          try {
            for (int j = 0; j < 1000; j++) {
              String str = testdata[i2];
              String str2 = null;
              /*synchronized(df)*/ {
                Date d = df.parse(str);
                str2 = df.format(d);
              }
              if (!str.equals(str2)) {
                throw new RuntimeException("date conversion failed after " + j + " iterations. Expected " + str + " but got " + str2);
              }
            }
          } catch (ParseException e) {
            throw new RuntimeException("parse failed");
          }
        }
      };
      new Thread(r[i]).start();
    }
  }
}


Tworzymy 3 wątki, a każdy z nich 1000 razy parsuje tę samą datę. Wszystkie wątki korzystają z tego samego obiektu klasy SimpleDateFormat - skończy się to niestety błędem. U mnie przykładowo wyskakuje wyjątek:

date conversion failed after 4 iterations. Expected 01-01-1999 but got 14-02-1970

Hm... Ale przecież roku 1970 nie ma nawet w żadnej z dat z listy testowej. Jednocześnie niepokojący jest fakt, że tak naprawdę data została sparsowana i nie został rzucony żaden wyjątek. My po prostu wiemy, jaki powinien być wynik i rzucamy wyjątek jeżeli nie jest on poprawny. Jeżeli ktoś napotka błąd "Czasami system wstawi złą datę do bazy" w większym systemie, to zdiagnozowanie SimpleDateFormat przyczyny zapewne graniczy z cudem.

Sprawnie przeglądający kod pewnie zauważyli wykomentowane synchronized(df) - tak, synchronizowany dostęp do parsera daty załatwia sprawę. W praktyce można podobny efekt osiągnąć prościutką klasą opakowującą:

public class ThreadSafeSimpleDateFormat {

  private DateFormat df;

  public ThreadSafeSimpleDateFormat(String format) {
    this.df = new SimpleDateFormat(format);
  }

  public synchronized String format(Date date) {
    return df.format(date);
  }

  public synchronized Date parse(String string) throws ParseException {
    return df.parse(string);
  }
}


Wystarczy teraz zamiana linijki:

static SimpleDateFormat df = new SimpleDateFormat("dd-mm-yyyy");

na

static ThreadSafeSimpleDateFormat df = new ThreadSafeSimpleDateFormat("dd-mm-yyyy");

i powyższy problematyczny kawałek kodu już działa :) Czyli sprawa może zostać załatwiona dzięki jednej modyfikacji w naszej klasie GlobalConst ;)

Mały update: Aby uprzedzić pytania: niestety, napisanie

static volatile SimpleDateFormat df = new SimpleDateFormat("dd-mm-yyyy");

nie pomoże - mimo modyfikatora volatile kod nadal nie będzie wielowątkowy

Więcej: http://www.codefutures.com/weblog/andygrove/2007/10/simpledateformat-and-thread-safety.html

3 komentarze:

  1. Jedna uwaga: w swoich wpisach często umieszczasz sporo kodu źródłowego - OK. Jednak przydałoby się poprawić jakoś jego czytelność. Za mała czcionka, brak kolorowania składni...

    OdpowiedzUsuń
  2. Problem małej czcionki leży w arkuszach CSS blogspota - w Firefoksie, Operze, IE ma ona normalny rozmiar, w Chrome jest faktycznie bardzo mała. Na dowód screenshot z FF:

    http://img829.imageshack.us/img829/167/czcionka.jpg

    Sam kod formatuje wykorzystując tylko HTMLowy znacznik <tt> - jak będę miał czas to rozejrzę się za czymś, co pozwoliłoby kolorować składnię i było zgodne z każdą przeglądarką ;)

    OdpowiedzUsuń
  3. Najlepszym rozwiązaniem jest użycie org.apache.commons.lang.time.FastDateFormat

    OdpowiedzUsuń