5 Mart 2014 Çarşamba

Thread-Safe Olmayan SimpleDateFormat İçin Bir Çözüm : ThreadLocal

Bir uygulamamızda, standart tarih formatı oluşturmak için, aşağıdaki gibi bir BaseService class’ımızın içinde static date formatter’lar oluşturmuştuk:

public class BaseService { protected static final SimpleDateFormat SDF_DT = new SimpleDateFormat("dd.MM.yyyy HH:mm"); protected static final SimpleDateFormat SDF_D = new SimpleDateFormat("dd.MM.yyyy"); …

Daha sonra, bu formatter’ları, implement eden her class içinde aşağıdaki gibi kullanıyorduk:
public class CampaignRegistrationService extends BaseService{ … oblg.setExpireDate((SDF_D.format(expireDate))); … time = SDF_D.parse(iaArchivedCriteria.getValue()).getTime(); …

Uygulamamızda her zaman değil ama bazen, yoğun işlemler sırasında, formatter’larımızı kullandığımız yerlerden aşağıdaki gibi çeşitli hataların atıldığını gördük.

java.lang.NumberFormatException: For input string: "" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48) at java.lang.Long.parseLong(Long.java:431) at java.lang.Long.parseLong(Long.java:468) at java.text.DigitList.getLong(DigitList.java:177) at java.text.DecimalFormat.parse(DecimalFormat.java:1297) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311) at java.text.DateFormat.parse(DateFormat.java:335) at ...

java.lang.NumberFormatException: multiple points at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1082) at java.lang.Double.parseDouble(Double.java:510) at java.text.DigitList.getDouble(DigitList.java:151) at java.text.DecimalFormat.parse(DecimalFormat.java:1302) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1934) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311) at java.text.DateFormat.parse(DateFormat.java:335) at ...

java.lang.ArrayIndexOutOfBoundsException: -1 at java.text.DigitList.fitsIntoLong(DigitList.java:212) at java.text.DecimalFormat.parse(DecimalFormat.java:1295) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1934) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311) at java.text.DateFormat.parse(DateFormat.java:335) at ...


Her defasında farklı farklı ve koda bakınca da pek anlamlı gelmeyen bu hatalarla ilgili araştırma yaptığımızda, sorunun thread-safe olmayan, yani farklı threadler tarafından ortak kullanılan alanlara sahip olan java.text.SimpleDateFormat class’ının kullanımından kaynaklandığını farkettik.

Sorunu daha detaylı açıklayacak olursak, SimpleDateFormat class'ı methodlardaki hesaplamalarında kullandığı ara değerleri instance alanlarda saklamaktadır. Yani bir instance iki thread tarafından kullanıldığında,  her biri birbirinin sonucunu bozabilmektedir. Örneğin, java.text.DateFormat’ın kaynak koduna bakacak olursak, DateFormat veya SimpleDateFormat içindeki işlemlerde kullanılmak üzere bir Calendar instance alanı olduğunu görürüz.

public abstract class DateFormat extends Format { /** * The calendar that <code>DateFormat</code> uses to produce the time field * values needed to implement date and time formatting. Subclasses should * initialize this to a calendar appropriate for the locale associated with * this <code>DateFormat</code>. * @serial */ protected Calendar calendar; …
DateFormat ‘ı extend eden SimpleDateFormat’ın içindeki parse() methoduna bakacak olursak, ilk olarak, calendar.clear() methodunun çalıştırıldığını daha sonra da calendar.add(..) methodunun çalıştırıldığını görürüz. Eğer bir thread’in parse() method çağrımı tamamlanmadan, bir başka thread de parse() methodunu çağırırsa, ilk thread’in beklediği değerin, ikinci thread tarafından silinmesi(clean) durumu ortaya çıkacaktır.

public class SimpleDateFormat extends DateFormat { … public Date parse(String text, ParsePosition pos) { … calendar.clear(); // Clears all the time fields … Date parsedDate = calendar.getTime(); if( ambiguousYear[0] && !parsedDate.after(defaultCenturyStart) ) { calendar.add(Calendar.YEAR, 100); parsedDate = calendar.getTime(); } …

Bu durum da bizim bazen doğru sonuçlar alırken, bazen de alakasız hatalar almamız durumunu açıklıyor.

Burada yaşadığımız problemi ThreadLocal ile çözdük. Java 2’den beri var olan ThreadLocal sınıfı thread-local değişkenleri sağlar. Bu değişkenler, her thread’e özel olarak tutulur. Farklı thread’lar birbirlerinin thread-local değişkenlerine erişemez.

ThreadLocal  içerisinde get, set, initialValue ve remove metotları bulunur.
get() : ThreadLocal değişkeninin değerini döndürür.
set(T value) : ThreadLocal değişkeninin değerini günceller.
initialValue() : ThreadLocal değişkeninin ilk değerini döndürür.
remove() : ThreadLocal değişkeninin değerini siler. (Java 5 ile eklendi)

ThreadLocal ile her thread’ın kendi formatter’ını kullandığı çözümümüzü aşağıda görebilirsiniz:

import java.text.SimpleDateFormat; /* * Thread Safe implementation of SimpleDateFormat * Each Thread will get its own instance of SimpleDateFormat which will not be shared between other threads. * */ public class PerThreadFormatter { private static final ThreadLocal<SimpleDateFormat> dateFormatHolder = new ThreadLocal<SimpleDateFormat>() { /* * initialValue() is called */ @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("dd.MM.yyyy"); } }; private static final ThreadLocal<SimpleDateFormat> dateTimeFormatHolder = new ThreadLocal<SimpleDateFormat>() { /* * initialValue() is called */ @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("dd.MM.yyyy HH:mm"); } }; /* * Every time there is a call for DateTimeFormat, ThreadLocal will return calling * Thread's copy of SimpleDateFormat */ public static SimpleDateFormat getDateTimeFormatter() { return dateTimeFormatHolder.get(); } /* * Every time there is a call for DateFormat, ThreadLocal will return calling * Thread's copy of SimpleDateFormat */ public static SimpleDateFormat getDateFormatter() { return dateFormatHolder.get(); } }

import java.text.SimpleDateFormat; public class BaseService { protected static SimpleDateFormat getSDF_DT() { return PerThreadFormatter.getDateTimeFormatter(); } protected static SimpleDateFormat getSDF_D() { return PerThreadFormatter.getDateFormatter(); } …

public class CampaignRegistrationService extends BaseService{ … oblg.setExpireDate((getSDF_D().format(expireDate))); … time = getSDF_D().parse(iaArchivedCriteria.getValue()).getTime(); …

Yararlandığım Kaynaklar:
DateFormat in a Multithreading Environment
ThreadLocal in Java - Example Program and Tutorial
SimpleDateFormat thread safety
Multiple exceptions thrown parsing date string


ali kemal taşçı

6 yorum:

Unknown dedi ki...

Çok önemli bir konuya parmak basmışsın, teşekkürler Ali Kemal :)

İrfan Çuha dedi ki...

Karşılaştığın sorunları ve çözümleri burada paylaşman çok güzel bir alışkanlık oldu sende :D Teşekkürler.

Halil Karaköse dedi ki...

threadlocal'in faydasini ifade eden guzel bir ornek olmus. Bilgi icin tesekkurler.

Cihan Sarp dedi ki...

Her thread icin bir tane SDF nesnesi yaratip bunun referanslarini saklamak yerine gerektiginde bir tane yaratip isin bitince de GC'nin bunun caresine bakmasini saglasan daha iyi olmaz mi? Bu sekilde SDF'yi 1 kez kullanan siniflar bile SDF referansini tutmaya devam edecekler.

. dedi ki...

Selam Cihan, öncelikle, çözüm önerin için teşekkürler. Bahsettiğin öneri performans açısından daha iyi olabilir. Buradaki çözüm nevcut kodda bir base class içinden kullanılan SDF'den dolayı hata alınmasının önüne hızlıca geçilmesi açısından faydalı oldu. Ama bahsettiğin gibi bir çözümü daha kapsamlı bir incelemeyle, kullanılmayan thread'lerin GC ile temizlenme sıklığı ile karşılaştırarak uygulayabiliriz...
Tekrar teşekkürler :)

. dedi ki...

Cahit, İrfan, Halil güzel yorumlarınız için ayrıca teşekkür ederim :)