„Java DateFormat nie jest bezpieczna wątkowo”, do czego to prowadzi?

143

Wszyscy ostrzegają, że Java DateFormat nie jest bezpieczna dla wątków i rozumiem tę koncepcję teoretycznie.

Ale nie jestem w stanie wyobrazić sobie, jakie faktyczne problemy możemy napotkać z tego powodu. Powiedzmy, że mam pole DateFormat w klasie i to samo jest używane w różnych metodach w klasie (formatowanie dat) w środowisku wielowątkowym.

Czy to spowoduje:

  • każdy wyjątek, taki jak wyjątek formatu
  • rozbieżność danych
  • jakikolwiek inny problem?

Proszę również wyjaśnić dlaczego.

haps10
źródło
1
Oto, do czego to prowadzi: stackoverflow.com/questions/14309607/ ...
caw
Teraz jest rok 2020. Przeprowadzając moje testy (równolegle) odkryłem, że data z jednego wątku jest przypadkowo zwracana, gdy inny wątek próbuje sformatować datę. Zajęło mi kilka tygodni, zanim zbadałem, od czego to zależy, dopóki nie znalazłem w programie formatującym, w którym konstruktor tworzy instancję kalendarza, a kalendarz jest później konfigurowany tak, aby przyjmował formatowaną przez nas datę. Czy to jeszcze rok 1990 w ich głowach? Kto wie.
Vlad Patryshev

Odpowiedzi:

262

Wypróbujmy to.

Oto program, w którym wiele wątków używa udostępnionego pliku 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());
    }
}

Uruchom to kilka razy, a zobaczysz:

Wyjątki :

Oto kilka przykładów:

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)

Nieprawidłowe wyniki :

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

Prawidłowe wyniki :

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

Innym podejściem do bezpiecznego używania DateFormats w środowisku wielowątkowym jest użycie ThreadLocalzmiennej do przechowywania DateFormat obiektu, co oznacza, że ​​każdy wątek będzie miał swoją własną kopię i nie będzie musiał czekać, aż inne wątki go zwolnią. Oto jak:

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

Oto dobry post z większą liczbą szczegółów.

dogbane
źródło
1
Uwielbiam tę odpowiedź :-)
Sundararaj Govindasamy
Myślę, że powodem, dla którego jest to tak frustrujące dla programistów, jest to, że na pierwszy rzut oka wygląda na to, że powinno to być wywołanie funkcji „zorientowane funkcjonalnie”. Np. Dla tego samego wejścia oczekuję tego samego wyjścia (nawet jeśli wywołuje to wiele wątków). Uważam, że odpowiedź sprowadza się do tego, że twórcy Javy nie docenili FOP w czasie, gdy pisali oryginalną logikę daty i czasu. Więc na końcu po prostu mówimy „nie ma powodu, dla którego tak jest, poza tym, że jest źle”.
Lezorte
30

Spodziewałbym się uszkodzenia danych - np. Jeśli analizujesz dwie daty w tym samym czasie, możesz mieć jedno połączenie zanieczyszczone danymi z innego.

Łatwo sobie wyobrazić, jak to się może stać: analizowanie często wymaga zachowania określonego stanu tego, co przeczytałeś do tej pory. Jeśli dwa wątki depczą w tym samym stanie, pojawią się problemy. Na przykład DateFormatujawnia calendarpole typu Calendari patrząc na kod SimpleDateFormat, wywołanie niektórych metod calendar.set(...)i wywołanie innych calendar.get(...). To oczywiście nie jest bezpieczne dla wątków.

Nie spojrzał w dokładnych szczegółach dlaczego DateFormatnie jest bezpieczny wątku, ale dla mnie to wystarczy, aby wiedzieć, że to jest niebezpieczne bez synchronizacji - dokładne sposoby braku bezpieczeństwa mogą się nawet zmieniać między wydaniami.

Osobiście użyłbym zamiast tego parserów z Joda Time , ponieważ są one bezpieczne dla wątków - a Joda Time jest znacznie lepszym API daty i czasu na początek :)

Jon Skeet
źródło
1
+1 jodatime i sonar, aby wymusić jego użycie: mestachs.wordpress.com/2012/03/17/ ...
mestachs
18

Jeśli używasz Java 8, możesz użyć DateTimeFormatter.

Formater utworzony z wzorca może być używany tyle razy, ile potrzeba, jest niezmienny i bezpieczny dla wątków.

Kod:

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

Wynik:

2017-04-17
cjungel
źródło
10

Mówiąc ogólnie, nie należy definiować DateFormatzmiennej jako wystąpienia obiektu, do którego ma dostęp wiele wątków, lub static.

Formaty dat nie są synchronizowane. Zaleca się tworzenie oddzielnych instancji formatu dla każdego wątku.

Tak więc, na wypadek, Foo.handleBar(..)gdybyś miał dostęp do wielu wątków, zamiast:

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

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

powinieneś użyć:

public class Foo {

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

Ponadto we wszystkich przypadkach nie należy mieć rozszerzenia static DateFormat

Jak zauważył Jon Skeet, możesz mieć zarówno statyczne, jak i współdzielone zmienne instancji w przypadku wykonywania zewnętrznej synchronizacji (tj. Używania synchronizedwokół wywołań DateFormat)

Bozho
źródło
2
Nie widzę, żeby to w ogóle następowało. Nie zapewniam, aby większość moich typów była bezpieczna dla wątków, więc nie oczekuję, że ich zmienne instancji również będą bezpieczne dla wątków. Bardziej rozsądne jest stwierdzenie, że nie powinieneś przechowywać DateFormat w zmiennej statycznej - lub jeśli to zrobisz, będziesz potrzebować synchronizacji.
Jon Skeet
1
Generalnie jest to lepsze - chociaż byłoby dobrze mieć statyczny format DateFormat, jeśli wykonałeś synchronizację. W wielu przypadkach może to działać lepiej niż tworzenie nowych SimpleDateFormatbardzo często. Będzie to zależeć od wzorca użytkowania.
Jon Skeet
1
Czy mógłbyś wyjaśnić, w jaki sposób i dlaczego instancja statyczna może powodować problemy w środowisku wielowątkowym?
Alexandr
4
ponieważ przechowuje obliczenia pośrednie w zmiennych instancji, a to nie jest bezpieczne dla
wątków
2

Formaty dat nie są synchronizowane. Zaleca się tworzenie oddzielnych instancji formatu dla każdego wątku. Jeśli wiele wątków jednocześnie uzyskuje dostęp do formatu, należy go zsynchronizować zewnętrznie.

Oznacza to, że załóżmy, że masz obiekt DateFormat i uzyskujesz dostęp do tego samego obiektu z dwóch różnych wątków i wywołujesz metodę formatowania na tym obiekcie oba wątki wejdą w tę samą metodę w tym samym czasie na tym samym obiekcie, abyś mógł zwizualizować, że wygrał nie daje właściwego wyniku

Jeśli musisz w jakikolwiek sposób pracować z DateFormatem, powinieneś coś zrobić

public synchronized myFormat(){
// call here actual format method
}
Jigar Joshi
źródło
1

Dane są uszkodzone. Wczoraj zauważyłem to w moim programie wielowątkowym, w którym miałem DateFormatobiekt statyczny i wywołałem jego format()wartości odczytywane przez JDBC. Miałem instrukcję wyboru SQL, w której przeczytałem tę samą datę z różnymi nazwami ( SELECT date_from, date_from AS date_from1 ...). Takie stwierdzenia były używane w 5 wątkach dla różnych dat w WHEREclasue. Daty wyglądały „normalnie”, ale różniły się wartością - podczas gdy wszystkie daty pochodziły z tego samego roku, tylko miesiąc i dzień ulegały zmianie.

Inne odpowiedzi pokazują, jak uniknąć takiej korupcji. Zrobiłem moje DateFormatnie statyczne, teraz jest członkiem klasy, która wywołuje instrukcje SQL. Testowałem również wersję statyczną z synchronizacją. Oba działały dobrze, bez różnicy w wydajności.

Michał Niklas
źródło
1

Specyfikacje Format, NumberFormat, DateFormat, MessageFormat itp. Nie zostały zaprojektowane tak, aby były bezpieczne dla wątków. Ponadto metoda parse wywołuje Calendar.clone()metodę i wpływa na ślady kalendarza, więc wiele wątków analizowanych jednocześnie zmieni klonowanie wystąpienia Calendar.

Co więcej, są to raporty o błędach, takie jak ten i ten , z wynikami problemu z bezpieczeństwem wątków DateFormat.

Buhake Sindi
źródło
1

W najlepszej odpowiedzi dogbane podał przykład użycia parsefunkcji i do czego prowadzi. Poniżej znajduje się kod, który pozwala sprawdzić formatdziałanie.

Zauważ, że jeśli zmienisz liczbę executorów (współbieżnych wątków), otrzymasz inne wyniki. Z moich eksperymentów:

  • Pozostaw newFixedThreadPoolwartość 5, a pętla za każdym razem zawiedzie.
  • Ustaw na 1, a pętla będzie zawsze działać (oczywiście, ponieważ wszystkie zadania są faktycznie uruchamiane jedno po drugim)
  • Ustaw na 2 i pętla ma tylko około 6% szans na zadziałanie.

Zgaduję, że YMMV w zależności od twojego procesora.

formatFunkcja nie przez czas formatowania z innym wątku. Dzieje się tak, ponieważ formatfunkcja wewnętrznie używa calendarobiektu, który jest ustawiony na początku formatfunkcji. A calendarobiekt jest własnością SimpleDateFormatklasy. Westchnienie...

/**
 * 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");
}
Nux
źródło
0

Jeśli istnieje wiele wątków manipulujących / uzyskujących dostęp do pojedynczej instancji DateFormat i synchronizacja nie jest używana, można uzyskać zaszyfrowane wyniki. Dzieje się tak, ponieważ wiele operacji nieatomowych może zmieniać stan lub widzieć pamięć niespójnie.

seand
źródło
0

To jest mój prosty kod, który pokazuje, że DateFormat nie jest bezpieczny dla wątków.

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

Ponieważ wszystkie wątki używają tego samego obiektu SimpleDateFormat, zgłasza następujący wyjątek.

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)

Ale jeśli przekażemy różne obiekty do różnych wątków, kod działa bez błędów.

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

Takie są wyniki.

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
Erangad
źródło
OP zapytał, dlaczego tak się dzieje i co.
Adam
0

To spowoduje ArrayIndexOutOfBoundsException

Oprócz nieprawidłowego wyniku, od czasu do czasu będzie się to zawieszać. To zależy od prędkości twojej maszyny; w moim laptopie zdarza się to średnio raz na 100 000 połączeń:

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

ostatnia linia może wywołać opóźniony wyjątek executora:

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)
epoks
źródło