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
wtorek, 28 września 2010
Subskrybuj:
Komentarze do posta (Atom)
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ń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:
OdpowiedzUsuń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ą ;)
Najlepszym rozwiązaniem jest użycie org.apache.commons.lang.time.FastDateFormat
OdpowiedzUsuń