“Java DateFormat threadsafe değildir” bu ne anlama geliyor?


143

Herkes Java DateFormat'ın iş parçacığı için güvenli olmadığı konusunda uyarıyor ve ben teorik olarak konsepti anlıyorum.

Ancak bu nedenle karşılaşabileceğimiz gerçek sorunları görselleştiremiyorum. Diyelim ki, bir sınıfta bir DateFormat alanı var ve aynı, çok iş parçacıklı bir ortamda sınıftaki farklı tarihlerde (biçimlendirme) kullanılır.

Bu neden olur:

  • biçim istisnası gibi herhangi bir istisna
  • verilerde tutarsızlık
  • başka sorun var mı?

Ayrıca, lütfen nedenini açıklayın.



Şimdi 2020. Testlerimi çalıştırarak (paralel olarak), başka bir iş parçacığı bir tarihi biçimlendirmeye çalışırken bir iş parçacığından bir tarihin rasgele döndürüldüğünü keşfetti. Bir kurucunun takvimi başlattığı bir biçimlendiricide bulunana ve takvim daha sonra biçimlendirdiğimiz tarihi alacak şekilde yapılandırılıncaya kadar, neye bağlı olduğunu araştırmam için birkaç hafta sürdü. Hala 1990'ların başında mı? Kim bilir.
Vlad Patryshev

Yanıtlar:


264

Hadi deneyelim.

Burada, birden çok iş parçacığının paylaşılan bir SimpleDateFormat .

Program :

public static void main(String[] args) throws Exception {

    final DateFormat format = new SimpleDateFormat("yyyyMMdd");

    Callable<Date> task = new Callable<Date>(){
        public Date call() throws Exception {
            return format.parse("20101022");
        }
    };

    //pool with 5 threads
    ExecutorService exec = Executors.newFixedThreadPool(5);
    List<Future<Date>> results = new ArrayList<Future<Date>>();

    //perform 10 date conversions
    for(int i = 0 ; i < 10 ; i++){
        results.add(exec.submit(task));
    }
    exec.shutdown();

    //look at the results
    for(Future<Date> result : results){
        System.out.println(result.get());
    }
}

Bunu birkaç kez çalıştırın ve göreceksiniz:

İstisnalar :

İşte birkaç örnek:

1.

Caused by: 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:1298)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)

2.

Caused by: java.lang.NumberFormatException: For input string: ".10201E.102014E4"
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1224)
    at java.lang.Double.parseDouble(Double.java:510)
    at java.text.DigitList.getDouble(DigitList.java:151)
    at java.text.DecimalFormat.parse(DecimalFormat.java:1303)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)

3.

Caused by: java.lang.NumberFormatException: multiple points
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1084)
    at java.lang.Double.parseDouble(Double.java:510)
    at java.text.DigitList.getDouble(DigitList.java:151)
    at java.text.DecimalFormat.parse(DecimalFormat.java:1303)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1936)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1312)

Yanlış Sonuçlar :

Sat Oct 22 00:00:00 BST 2011
Thu Jan 22 00:00:00 GMT 1970
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Thu Oct 22 00:00:00 GMT 1970
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010

Doğru Sonuçlar :

Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010

DateFormats'ı çok iş parçacıklı bir ortamda güvenli bir şekilde kullanmak için başka bir yaklaşım ThreadLocal, DateFormat nesneyi tutmak için bir değişken kullanmaktır . Bu nasıl:

public class DateFormatTest {

  private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>(){
    @Override
    protected DateFormat initialValue() {
        return new SimpleDateFormat("yyyyMMdd");
    }
  };

  public Date convert(String source) throws ParseException{
    Date d = df.get().parse(source);
    return d;
  }
}

İşte daha fazla ayrıntı ile iyi bir yazı .


1
Bu cevabı çok seviyorum :-)
Sundararaj Govindasamy

Bunun geliştiriciler için bu kadar sinir bozucu olmasının nedeni, ilk bakışta, 'işlevsel olarak yönlendirilmiş' bir işlev çağrısı olması gerektiği gibi görünüyor. Örneğin, aynı girdi için, aynı çıktıyı bekliyorum (birden fazla iş parçacığı çağırsa bile). İnandığım cevap, Java'nın geliştiricilerine, orijinal tarih zaman mantığını yazdıkları sırada FOP için bir takdir sahibi olmamaları. Sonuçta, diyoruz ki "bunun yanlış olmasından başka bir nedeni yok".
Lezorte

30

Veri bozulmasını beklerim - örneğin, aynı anda iki tarihi ayrıştırıyorsanız, bir çağrının başka bir veriyle kirletilmesini sağlayabilirsiniz.

Bunun nasıl olabileceğini hayal etmek kolaydır: Ayrıştırma, şu ana kadar okuduklarınızla ilgili olarak belirli bir miktarda devletin korunmasını içerir. İki iş parçacığı aynı durumda çiğniyorsa, sorunlarla karşılaşırsınız. Örneğin , bazı yöntemlerin çağrıldığı ve diğerlerinin çağırdığı türden DateFormatbir calendaralanı ortaya çıkarır Calendarve koduna bakar . Bu açıkça iş parçacığı için güvenli değildir.SimpleDateFormatcalendar.set(...)calendar.get(...)

Ben bakmadım kesin neden ayrıntıları DateFormatevreli değil, ama benim için o bilmek yeterlidir olan senkronizasyon gerekmeden güvensiz - olmayan emniyet kesin görgü bile bültenleri arasında değişebilir.

Şahsen ben den ayrıştırıcıları kullanmak Joda Zamanda onlar yerine bulunmaktadır parçacığı güvenli - ve Joda Zaman ile başlamak çok daha iyi bir tarih ve saat API'sıdır :)


1
+1 jodatime ve sonar kullanımını zorlayacak: mestachs.wordpress.com/2012/03/17/…
mestachs

18

Java 8 kullanıyorsanız, kullanabilirsiniz DateTimeFormatter.

Bir desenden oluşturulan bir formatlayıcı gerektiği kadar kullanılabilir, değiştirilemez ve diş açmaya karşı güvenlidir.

Kod:

LocalDate date = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String text = date.format(formatter);
System.out.println(text);

Çıktı:

2017-04-17

10

Kabaca, DateFormatbirçok iş parçacığı tarafından erişilen bir nesnenin örnek değişkeni olarak tanımlamamanız gerekir static.

Tarih biçimleri senkronize edilmedi. Her bir iş parçacığı için ayrı format örnekleri oluşturmanız önerilir.

Bu nedenle, adresinize Foo.handleBar(..)aşağıdakiler yerine birden çok iş parçacığı tarafından erişilmesi durumunda :

public class Foo {
    private DateFormat df = new SimpleDateFormat("dd/mm/yyyy");

    public void handleBar(Bar bar) {
        bar.setFormattedDate(df.format(bar.getStringDate());  
    }
}

kullanmalısın:

public class Foo {

    public void handleBar(Bar bar) {
        DateFormat df = new SimpleDateFormat("dd/mm/yyyy");
        bar.setFormattedDate(df.format(bar.getStringDate());  
    }
}

Ayrıca, her durumda, static DateFormat

Jon Skeet tarafından belirtildiği gibi, statik ve durumda paylaşılan örnek değişkenleri harici senkronizasyon (yani kullanımını gerçekleştirmek ikisine de sahip olabilir synchronizedçağrıları etrafında DateFormat)


2
Bunun hiç izlediğini görmüyorum. Benim türlerinin çoğu iş parçacığı güvenli yapmıyorum, bu yüzden onların örnek değişkenleri de iş parçacığı için güvenli olması beklemeyin. Bir DateFormat öğesini statik bir değişkende saklamamanız gerektiğini söylemek daha mantıklıdır - ya da bunu yaparsanız senkronizasyona ihtiyacınız olacaktır.
Jon Skeet

1
Böylesi daha genel olarak değil - eğer statik DATEFORMAT olması iyi olacağını rağmen yaptığımız senkronize. Bu, çoğu durumda SimpleDateFormatçok sık yeni bir şey oluşturmaktan daha iyi performans gösterebilir . Kullanım şekline bağlı olacaktır.
Jon Skeet

1
Statik örneğin çok iş parçacıklı bir ortamda nasıl ve neden sorunlara neden olabileceğini açıklar mısınız?
Alexandr

4
çünkü ara hesaplamaları örnek değişkenlerde saklar ve bu iş parçacığı için güvenli değildir
Bozho

2

Tarih biçimleri senkronize edilmedi. Her bir iş parçacığı için ayrı format örnekleri oluşturmanız önerilir. Birden çok iş parçacığı bir biçime aynı anda erişiyorsa, dış olarak eşitlenmelidir.

Bu, DateFormat'ın bir nesnesine sahip olduğunuzu ve iki farklı iş parçacığından aynı nesneye eriştiğinizi ve bu nesnenin üzerine her iki iş parçacığının aynı nesneye aynı anda gireceğini ve böylece kazandığını görselleştirebileceğiniz anlamına gelir. doğru sonuç vermiyor

DateFormat ile herhangi bir şekilde çalışmak zorundaysanız, o zaman bir şey yapmalısınız

public synchronized myFormat(){
// call here actual format method
}

1

Veriler bozuk. Dün bunu statik DateFormatnesneye sahip olduğum çok iş parçacıklı programımda fark ettim ve format()JDBC aracılığıyla okunan değerler için çağırdım . Ben farklı adları ( SELECT date_from, date_from AS date_from1 ...) ile aynı tarih okumak SQL seçme deyimi vardı . Bu tür ifadeler, WHEREclasue'daki çeşitli tarihler için 5 iş parçacığında kullanılıyordu . Tarihler "normal" görünüyordu, ancak değerler farklıydı - tüm tarihler aynı yıldan sadece ay ve gün değişti.

Diğer cevaplar size bu tür yolsuzluklardan kaçınmanın yolunu gösterir. Benim DateFormatstatik değil, şimdi SQL ifadeleri çağıran bir sınıfın üyesi. Senkronizasyon ile statik versiyonu da test ettim. Her ikisi de performans farkı olmadan iyi çalıştı.


1

Format, NumberFormat, DateFormat, MessageFormat vb. Özellikleri iş parçacığı için güvenli olacak şekilde tasarlanmamıştır. Ayrıca, ayrıştırma yöntemi yöntemi çağırır Calendar.clone()ve eşzamanlı olarak ayrıştırma yapan birçok iş parçacığı Takvim örneğinin klonlamasını değiştirir.

Daha fazlası için, bunlar DateFormat iş parçacığı güvenliği sorununun sonuçlarıyla birlikte, bunun ve bunun gibi hata raporlarıdır .


1

En iyi cevapta dogbane parseişlevi kullanma ve neye yol açtığına dair bir örnek verdi . Aşağıda formatişlevi kontrol etmenizi sağlayan bir kod var .

İcracı sayısını (eşzamanlı iş parçacığı) değiştirirseniz farklı sonuçlar alacağınıza dikkat edin. Deneylerimden:

  • newFixedThreadPool5'e ayarlı olarak bırakın , döngü her seferinde başarısız olur.
  • 1 olarak ayarlandığında döngü her zaman çalışır (açıkçası tüm görevler tek tek çalıştırıldığı için)
  • 2'ye ayarlandığında, döngü sadece yaklaşık% 6 çalışma şansına sahiptir.

İşlemcinize bağlı olarak YMMV'yi tahmin ediyorum.

formatFonksiyon, farklı bir iplikten zaman biçimlendirerek başarısız olur. Bunun nedeni, dahili formatişlevin, işlevin calendarbaşında ayarlanan nesneyi kullanmasıdır format. Ve calendarnesne SimpleDateFormatsınıfın bir özelliğidir . İç çekmek...

/**
 * Test SimpleDateFormat.format (non) thread-safety.
 *
 * @throws Exception
 */
private static void testFormatterSafety() throws Exception {
    final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    final Calendar calendar1 = new GregorianCalendar(2013,1,28,13,24,56);
    final Calendar calendar2 = new GregorianCalendar(2014,1,28,13,24,56);
    String expected[] = {"2013-02-28 13:24:56", "2014-02-28 13:24:56"};

    Callable<String> task1 = new Callable<String>() {
        @Override
        public String call() throws Exception {
            return "0#" + format.format(calendar1.getTime());
        }
    };
    Callable<String> task2 = new Callable<String>() {
        @Override
        public String call() throws Exception {
            return "1#" + format.format(calendar2.getTime());
        }
    };

    //pool with X threads
    // note that using more then CPU-threads will not give you a performance boost
    ExecutorService exec = Executors.newFixedThreadPool(5);
    List<Future<String>> results = new ArrayList<>();

    //perform some date conversions
    for (int i = 0; i < 1000; i++) {
        results.add(exec.submit(task1));
        results.add(exec.submit(task2));
    }
    exec.shutdown();

    //look at the results
    for (Future<String> result : results) {
        String answer = result.get();
        String[] split = answer.split("#");
        Integer calendarNo = Integer.parseInt(split[0]);
        String formatted = split[1];
        if (!expected[calendarNo].equals(formatted)) {
            System.out.println("formatted: " + formatted);
            System.out.println("expected: " + expected[calendarNo]);
            System.out.println("answer: " + answer);
            throw new Exception("formatted != expected");
        /**
        } else {
            System.out.println("OK answer: " + answer);
        /**/
        }
    }
    System.out.println("OK: Loop finished");
}

0

Tek bir DateFormat örneğini işleyen / erişen birden çok iş parçacığı varsa ve eşitleme kullanılmıyorsa, karıştırılmış sonuçlar elde etmek mümkündür. Çünkü atom olmayan birçok işlem durumu değiştiriyor veya belleği tutarsız görüyor olabilir.


0

DateFormat'ın iş parçacığı için güvenli olmadığını gösteren basit kodum bu.

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

public class DateTimeChecker {
    static DateFormat df = new SimpleDateFormat("EEE MMM dd kk:mm:ss z yyyy", Locale.ENGLISH);
    public static void main(String args[]){
       String target1 = "Thu Sep 28 20:29:30 JST 2000";
       String target2 = "Thu Sep 28 20:29:30 JST 2001";
       String target3 = "Thu Sep 28 20:29:30 JST 2002";
       runThread(target1);
       runThread(target2);
       runThread(target3);
   }
   public static void runThread(String target){
       Runnable myRunnable = new Runnable(){
          public void run(){

            Date result = null;
            try {
                result = df.parse(target);
            } catch (ParseException e) {
                e.printStackTrace();
                System.out.println("Ecxfrt");
            }  
            System.out.println(Thread.currentThread().getName() + "  " + result);
         }
       };
       Thread thread = new Thread(myRunnable);

       thread.start();
     }
}

Tüm iş parçacıkları aynı SimpleDateFormat nesnesini kullandığından, aşağıdaki özel durumu atar.

Exception in thread "Thread-0" Exception in thread "Thread-2" Exception in thread "Thread-1" java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(Unknown Source)
at sun.misc.FloatingDecimal.parseDouble(Unknown Source)
at java.lang.Double.parseDouble(Unknown Source)
at java.text.DigitList.getDouble(Unknown Source)
at java.text.DecimalFormat.parse(Unknown Source)
at java.text.SimpleDateFormat.subParse(Unknown Source)
at java.text.SimpleDateFormat.parse(Unknown Source)
at java.text.DateFormat.parse(Unknown Source)
at DateTimeChecker$1.run(DateTimeChecker.java:24)
at java.lang.Thread.run(Unknown Source)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(Unknown Source)
at sun.misc.FloatingDecimal.parseDouble(Unknown Source)
at java.lang.Double.parseDouble(Unknown Source)
at java.text.DigitList.getDouble(Unknown Source)
at java.text.DecimalFormat.parse(Unknown Source)
at java.text.SimpleDateFormat.subParse(Unknown Source)
at java.text.SimpleDateFormat.parse(Unknown Source)
at java.text.DateFormat.parse(Unknown Source)
at DateTimeChecker$1.run(DateTimeChecker.java:24)
at java.lang.Thread.run(Unknown Source)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(Unknown Source)
at sun.misc.FloatingDecimal.parseDouble(Unknown Source)
at java.lang.Double.parseDouble(Unknown Source)
at java.text.DigitList.getDouble(Unknown Source)
at java.text.DecimalFormat.parse(Unknown Source)
at java.text.SimpleDateFormat.subParse(Unknown Source)
at java.text.SimpleDateFormat.parse(Unknown Source)
at java.text.DateFormat.parse(Unknown Source)
at DateTimeChecker$1.run(DateTimeChecker.java:24)
at java.lang.Thread.run(Unknown Source)

Ancak, farklı iş parçacıklarına farklı nesneler iletirsek, kod hatasız çalışır.

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

public class DateTimeChecker {
    static DateFormat df;
    public static void main(String args[]){
       String target1 = "Thu Sep 28 20:29:30 JST 2000";
       String target2 = "Thu Sep 28 20:29:30 JST 2001";
       String target3 = "Thu Sep 28 20:29:30 JST 2002";
       df = new SimpleDateFormat("EEE MMM dd kk:mm:ss z yyyy", Locale.ENGLISH);
       runThread(target1, df);
       df = new SimpleDateFormat("EEE MMM dd kk:mm:ss z yyyy", Locale.ENGLISH);
       runThread(target2, df);
       df = new SimpleDateFormat("EEE MMM dd kk:mm:ss z yyyy", Locale.ENGLISH);
       runThread(target3, df);
   }
   public static void runThread(String target, DateFormat df){
      Runnable myRunnable = new Runnable(){
        public void run(){

            Date result = null;
            try {
                result = df.parse(target);
            } catch (ParseException e) {
                e.printStackTrace();
                System.out.println("Ecxfrt");
            }  
            System.out.println(Thread.currentThread().getName() + "  " + result);
         }
       };
       Thread thread = new Thread(myRunnable);

       thread.start();
   }
}

Bunlar sonuçlar.

Thread-0  Thu Sep 28 17:29:30 IST 2000
Thread-2  Sat Sep 28 17:29:30 IST 2002
Thread-1  Fri Sep 28 17:29:30 IST 2001

OP bunun neden olduğunu ve ne olduğunu sordu.
Adam

0

Bu neden olur ArrayIndexOutOfBoundsException

Yanlış sonuç dışında, zaman zaman size bir çökme verecektir. Makinenizin hızına bağlıdır; dizüstü bilgisayarımda, ortalama 100.000 çağrıda bir kez oluyor:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

ExecutorService executorService = Executors.newFixedThreadPool(2);
Future<?> future1 = executorService.submit(() -> {
  for (int i = 0; i < 99000; i++) {
    sdf.format(Date.from(LocalDate.parse("2019-12-31").atStartOfDay().toInstant(UTC)));
  }
});

executorService.submit(() -> {
  for (int i = 0; i < 99000; i++) {
    sdf.format(Date.from(LocalDate.parse("2020-04-17").atStartOfDay().toInstant(UTC)));
  }
});

future1.get();

son satır ertelenen yürütücü istisnasını tetikler:

java.lang.ArrayIndexOutOfBoundsException: Index 16 out of bounds for length 13
  at java.base/sun.util.calendar.BaseCalendar.getCalendarDateFromFixedDate(BaseCalendar.java:453)
  at java.base/java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2394)
  at java.base/java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2309)
  at java.base/java.util.Calendar.complete(Calendar.java:2301)
  at java.base/java.util.Calendar.get(Calendar.java:1856)
  at java.base/java.text.SimpleDateFormat.subFormat(SimpleDateFormat.java:1150)
  at java.base/java.text.SimpleDateFormat.format(SimpleDateFormat.java:997)
  at java.base/java.text.SimpleDateFormat.format(SimpleDateFormat.java:967)
  at java.base/java.text.DateFormat.format(DateFormat.java:374)
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.