"Java DateFormat non è thread-safe" a cosa porta questo?


143

Tutti avvertono che Java DateFormat non è thread-safe e capisco il concetto teoricamente.

Ma non sono in grado di visualizzare quali problemi reali possiamo affrontare a causa di questo. Ad esempio, ho un campo DateFormat in una classe e lo stesso viene utilizzato in diversi metodi della classe (date di formattazione) in un ambiente multi-thread.

Questo causerà:

  • qualsiasi eccezione come l'eccezione di formato
  • discrepanza nei dati
  • qualche altro problema?

Inoltre, spiega perché.


1
Questo è ciò che porta a: stackoverflow.com/questions/14309607/…
caw

Adesso è il 2020. Eseguendo i miei test (in parallelo) ho scoperto che una data da un thread viene restituita casualmente quando un altro thread tenta di formattare una data. Mi ci sono voluti un paio di settimane per indagare su ciò che dipende, fino a quando non si è trovato in un formattatore che un costruttore crea un'istanza di un calendario e il calendario viene successivamente configurato per prendere la data che formattiamo. È ancora il 1990 nelle loro teste? Chissà.
Vlad Patryshev,

Risposte:


263

Proviamolo.

Ecco un programma in cui più thread usano un condiviso SimpleDateFormat.

Programma :

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

Esegui alcune volte e vedrai:

Eccezioni :

Ecco alcuni esempi:

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)

Risultati errati :

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

Risultati corretti :

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

Un altro approccio per utilizzare DateFormats in modo sicuro in un ambiente multi-thread è quello di utilizzare una ThreadLocalvariabile per contenere l' DateFormat oggetto, il che significa che ogni thread avrà la propria copia e non dovrà attendere che altri thread lo rilascino. Questo è come:

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

Ecco un buon post con maggiori dettagli.


1
Adoro questa risposta :-)
Sundararaj Govindasamy il

Penso che il motivo per cui questo è così frustrante per gli sviluppatori sia che a prima vista sembra che dovrebbe essere una chiamata di funzione "orientata funzionalmente". Ad esempio per lo stesso input, mi aspetto lo stesso output (anche se più thread lo chiamano). La risposta, credo, arriva agli sviluppatori di Java che non hanno apprezzato la FOP nel momento in cui hanno scritto la logica dell'ora della data originale. Quindi, alla fine, diciamo semplicemente "non c'è motivo per cui sia così diverso da quello sbagliato".
Lezorte,

30

Mi aspetto un danneggiamento dei dati, ad esempio se stai analizzando due date contemporaneamente, potresti avere una chiamata inquinata da dati provenienti da un'altra.

È facile immaginare come ciò possa accadere: l'analisi spesso implica il mantenimento di un certo stato in merito a ciò che hai letto finora. Se due thread calpestano entrambi sullo stesso stato, otterrai problemi. Ad esempio, DateFormatespone un calendarcampo di tipo Calendare guardando il codice di SimpleDateFormat, alcuni metodi call calendar.set(...)e altri call calendar.get(...). Questo chiaramente non è thread-safe.

Non ho esaminato i dettagli esatti del perché DateFormatnon è thread-safe, ma per me è sufficiente sapere che non è sicuro senza sincronizzazione: le modalità esatte di non sicurezza potrebbero persino cambiare tra le versioni.

Personalmente userei invece i parser di Joda Time , poiché sono thread-safe - e Joda Time è un'API di data e ora molto migliore per iniziare :)


1
+1 jodatime e sonar per imporne l'utilizzo: mestachs.wordpress.com/2012/03/17/…
mestachs

18

Se si utilizza Java 8, è possibile utilizzare DateTimeFormatter.

Un formattatore creato da un modello può essere utilizzato tutte le volte che è necessario, è immutabile e sicuro per i thread.

Codice:

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

Produzione:

2017-04-17

10

In pratica, non dovresti definire una DateFormatvariabile di istanza di un oggetto a cui accedono molti thread, oppure static.

I formati della data non sono sincronizzati. Si consiglia di creare istanze di formato separate per ogni thread.

Quindi, nel caso in cui Foo.handleBar(..)si acceda a più thread, anziché a:

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

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

dovresti usare:

public class Foo {

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

Inoltre, in tutti i casi, non hai a static DateFormat

Come notato da Jon Skeet, è possibile avere variabili di istanza sia statiche che condivise nel caso in cui si esegua una sincronizzazione esterna (ovvero utilizzare synchronizedattorno alle chiamate al DateFormat)


2
Non vedo che ciò segua affatto. Non rendo la maggior parte dei miei tipi thread-safe, quindi non mi aspetto che le loro variabili di istanza siano thread-safe, necessariamente. È più ragionevole dire che non dovresti archiviare un DateFormat in una variabile statica - o, se lo fai, avrai bisogno della sincronizzazione.
Jon Skeet,

1
Questo è generalmente migliore - anche se sarebbe bene avere un DateFormat statica se fatto sincronizzazione. Ciò potrebbe comportare prestazioni migliori in molti casi rispetto alla creazione di una nuova SimpleDateFormatmolto spesso. Dipenderà dal modello di utilizzo.
Jon Skeet,

1
Potresti spiegare come e perché l'istanza statica può causare problemi in un ambiente multi-thread?
Alexandr,

4
perché memorizza i calcoli intermedi nelle variabili di istanza e non è sicuro per i thread
Bozho,

2

I formati della data non sono sincronizzati. Si consiglia di creare istanze di formato separate per ogni thread. Se più thread accedono contemporaneamente a un formato, è necessario sincronizzarlo esternamente.

Ciò significa che supponiamo di avere un oggetto di DateFormat e di accedere allo stesso oggetto da due thread diversi e di chiamare il metodo di formato su quell'oggetto entrambi i thread entreranno sullo stesso metodo contemporaneamente sullo stesso oggetto in modo da poterlo visualizzare vinto si tradurrà in un risultato corretto

Se devi lavorare con DateFormat come, allora dovresti fare qualcosa

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

1

I dati sono danneggiati. Ieri l'ho notato nel mio programma multithread in cui avevo un DateFormatoggetto statico e l' ho chiamato format()per i valori letti tramite JDBC. Ho avuto l'istruzione select SQL in cui ho letto la stessa data con nomi diversi ( SELECT date_from, date_from AS date_from1 ...). Dichiarazioni del genere stavano usando in 5 discussioni per varie date nel WHEREclasue. Le date sembravano "normali" ma differivano in valore - mentre tutte le date erano dello stesso anno solo il mese e il giorno cambiati.

Altre risposte ti mostrano come evitare tale corruzione. Ho reso il mio DateFormatnon statico, ora è un membro di una classe che chiama istruzioni SQL. Ho provato anche la versione statica con la sincronizzazione. Entrambi hanno funzionato bene senza alcuna differenza nelle prestazioni.


1

Le specifiche di Format, NumberFormat, DateFormat, MessageFormat, ecc. Non sono state progettate per essere thread-safe. Inoltre, il metodo di analisi chiama il Calendar.clone()metodo e influisce sulle impronte del calendario, quindi molti thread che analizzano contemporaneamente cambieranno la clonazione dell'istanza di Calendar.

Per di più, si tratta di segnalazioni di bug come questo e questo , con risultati del problema di sicurezza del thread DateFormat.


1

Nella migliore risposta dogbane ha fornito un esempio dell'uso della parsefunzione e di ciò che conduce. Di seguito è riportato un codice che consente di verificare la formatfunzione.

Si noti che se si modifica il numero di esecutori (thread simultanei) si otterranno risultati diversi. Dai miei esperimenti:

  • Lasciare newFixedThreadPoolimpostato su 5 e il loop fallirà ogni volta.
  • Impostato su 1 e il ciclo funzionerà sempre (ovviamente poiché tutte le attività vengono effettivamente eseguite una per una)
  • Impostato su 2 e il loop ha solo circa il 6% di probabilità di funzionare.

Sto indovinando YMMV a seconda del tuo processore.

La formatfunzione ha esito negativo formattando il tempo da un thread diverso. Questo perché la formatfunzione interna utilizza l' calendaroggetto che è impostato all'inizio della formatfunzione. E l' calendaroggetto è una proprietà della SimpleDateFormatclasse. Sospiro...

/**
 * 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

Se ci sono più thread che manipolano / accedono a una singola istanza DateFormat e la sincronizzazione non viene utilizzata, è possibile ottenere risultati confusi. Questo perché più operazioni non atomiche potrebbero cambiare stato o vedere la memoria in modo incoerente.


0

Questo è il mio semplice codice che mostra che DateFormat non è thread-safe.

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

Poiché tutti i thread utilizzano lo stesso oggetto SimpleDateFormat, genera la seguente eccezione.

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)

Ma se passiamo oggetti diversi a thread diversi, il codice viene eseguito senza errori.

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

Questi sono i risultati

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

L'OP ha chiesto perché questo accada e cosa.
Adam,

0

Questo causerà ArrayIndexOutOfBoundsException

A parte il risultato errato, di tanto in tanto ti causerà un arresto anomalo. Dipende dalla velocità della tua macchina; sul mio laptop, succede in media una volta su 100.000 chiamate:

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

l'ultima riga potrebbe attivare l'eccezione di esecutore posticipata:

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)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.