“O Java DateFormat não é seguro para threads”, o que isso leva?


143

Todo mundo adverte que o Java DateFormat não é seguro para threads e eu entendo o conceito teoricamente.

Mas não consigo visualizar quais problemas reais podemos enfrentar devido a isso. Digamos, eu tenho um campo DateFormat em uma classe e o mesmo é usado em métodos diferentes na classe (datas de formatação) em um ambiente com vários threads.

Isso causará:

  • qualquer exceção, como exceção de formato
  • discrepância nos dados
  • alguma outra questão?

Além disso, explique o porquê.



Agora é 2020. A execução de meus testes (em paralelo) descobriu que uma data de um thread é retornada casualmente quando outro thread está tentando formatar uma data. Demorei algumas semanas para investigar do que isso depende, até ser encontrado em um formatador que um construtor instancia um calendário, e o calendário é configurado posteriormente para levar a data que formatamos. Ainda é 1990 em suas cabeças? Quem sabe.
Vlad Patryshev

Respostas:


263

Vamos tentar.

Aqui está um programa no qual vários threads usam um compartilhado SimpleDateFormat.

Programa :

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

Execute isso algumas vezes e você verá:

Exceções :

Aqui estão alguns exemplos:

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)

Resultados incorretos :

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

Resultados corretos :

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

Outra abordagem para usar com segurança o DateFormats em um ambiente com vários threads é usar uma ThreadLocalvariável para manter o DateFormat objeto, o que significa que cada thread terá sua própria cópia e não precisará esperar que outros threads o liberem. É assim:

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

Aqui está um bom post com mais detalhes.


1
Eu amo esta resposta :-)
Sundararaj Govindasamy

Eu acho que a razão pela qual isso é tão frustrante para os desenvolvedores é que, à primeira vista, parece que deve ser uma chamada de função 'funcionalmente orientada'. Por exemplo, para a mesma entrada, espero a mesma saída (mesmo que vários threads chamem). A resposta que eu acredito se resume aos desenvolvedores de Java que não apreciam a FOP no momento em que escreveram a lógica original de data e hora. Então, no final, apenas dizemos "não há razão para que isso seja diferente do que está errado".
Lezorte 20/09/19

30

Eu esperaria corrupção de dados - por exemplo, se você estiver analisando duas datas ao mesmo tempo, poderá ter uma chamada poluída por dados de outra.

É fácil imaginar como isso poderia acontecer: a análise geralmente envolve a manutenção de uma certa quantidade de estado sobre o que você leu até agora. Se dois threads estiverem pisando no mesmo estado, você terá problemas. Por exemplo, DateFormatexpõe um calendarcampo do tipo Calendare, observando o código de SimpleDateFormat, alguns métodos chamam calendar.set(...)e outros chamam calendar.get(...). Claramente, isso não é seguro para threads.

Não examinei os detalhes exatos de por que DateFormatnão é seguro para threads, mas para mim basta saber que é inseguro sem sincronização - as maneiras exatas de não segurança podem até mudar entre os lançamentos.

Pessoalmente, eu usaria os analisadores do Joda Time , pois eles são seguros para threads - e o Joda Time é uma API de data e hora muito melhor para começar :)


1
+1 JodaTime e sonar para impor a sua utilização: mestachs.wordpress.com/2012/03/17/...
mestachs

18

Se você estiver usando o Java 8, poderá usar DateTimeFormatter.

Um formatador criado a partir de um padrão pode ser usado quantas vezes for necessário, é imutável e é seguro para threads.

Código:

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

Resultado:

2017-04-17

10

Grosso modo, você não deve definir uma DateFormatvariável de instância como de um objeto acessado por vários threads ou static.

Os formatos de data não são sincronizados. É recomendável criar instâncias de formato separadas para cada thread.

Portanto, caso o seu Foo.handleBar(..)seja acessado por vários threads, em vez de:

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

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

você deveria usar:

public class Foo {

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

Além disso, em todos os casos, não possui um static DateFormat

Conforme observado por Jon Skeet, você pode ter variáveis ​​estáticas e uma instância compartilhada, caso execute sincronização externa (por exemplo, use as synchronizedchamadas para o DateFormat)


2
Não vejo o que se segue. Eu não faço a maioria dos meus tipos seguros para threads, então não espero que suas variáveis ​​de instância sejam seguras para thread, necessariamente. É mais razoável dizer que você não deve armazenar um DateFormat em uma variável estática - ou, se o fizer, precisará de sincronização.
quer

1
Isso é geralmente melhor - apesar de que seria bom ter um DateFormat estática se você fez sincronizar. Isso pode ter um desempenho melhor em muitos casos do que criar um novo com SimpleDateFormatmuita frequência. Depende do padrão de uso.
precisa

1
Você poderia explicar como e por que a instância estática pode causar problemas em um ambiente multithread?
Alexandr

4
porque armazena cálculos intermediários em variáveis ​​de instância, e isso não é seguro para
threads

2

Os formatos de data não são sincronizados. É recomendável criar instâncias de formato separadas para cada thread. Se vários threads acessarem um formato simultaneamente, ele deverá ser sincronizado externamente.

Isso significa que, suponha que você tenha um objeto DateFormat e esteja acessando o mesmo objeto a partir de dois threads diferentes e esteja chamando o método format sobre esse objeto, o thread entrará no mesmo método ao mesmo tempo no mesmo objeto para que você possa visualizá-lo resultar em resultado adequado

Se você tiver que trabalhar com o DateFormat, como deve fazer alguma coisa

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

1

Os dados estão corrompidos. Ontem eu notei isso no meu programa multithread, onde eu tinha um DateFormatobjeto estático e chamei format()de valores lidos via JDBC. Eu tinha a instrução SQL select, onde li a mesma data com nomes diferentes ( SELECT date_from, date_from AS date_from1 ...). Tais declarações foram usadas em 5 threads por várias datas noWHERE usadas clasue. As datas pareciam "normais", mas diferiam em valor - enquanto todas as datas eram do mesmo ano, apenas o mês e o dia eram alterados.

Outras respostas mostram o caminho para evitar essa corrupção. Eu fiz meu DateFormatnão estático, agora é um membro de uma classe que chama instruções SQL. Testei também a versão estática com a sincronização. Ambos funcionaram bem, sem diferença no desempenho.


1

As especificações de Format, NumberFormat, DateFormat, MessageFormat etc. não foram projetadas para serem seguras ao thread. Além disso, o método de análise chama o Calendar.clone()método e afeta as pegadas do calendário. Muitos segmentos analisados ​​simultaneamente alteram a clonagem da instância do Calendário.

Para mais, estes são relatórios de erros como este e este , com resultados do problema de segurança de thread do DateFormat.


1

Na melhor resposta, o dogbane deu um exemplo do uso da parsefunção e do que isso leva. Abaixo está um código que permite verificar a formatfunção.

Observe que, se você alterar o número de executores (threads simultâneos), obterá resultados diferentes. Dos meus experimentos:

  • Deixe newFixedThreadPooldefinido como 5 e o loop falhará sempre.
  • Defina como 1 e o loop sempre funcionará (obviamente, como todas as tarefas são executadas uma a uma)
  • Defina como 2 e o loop tem apenas cerca de 6% de chance de funcionar.

Eu estou supondo que YMMV, dependendo do seu processador.

A formatfunção falha ao formatar a hora de um thread diferente. Isso ocorre porque a formatfunção internamente está usando o calendarobjeto configurado no início da formatfunção. E o calendarobjeto é uma propriedade da SimpleDateFormatclasse. Suspiro...

/**
 * 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 houver vários threads manipulando / acessando uma única instância DateFormat e a sincronização não for usada, é possível obter resultados embaralhados. Isso ocorre porque várias operações não atômicas podem estar mudando de estado ou vendo a memória inconsistentemente.


0

Este é o meu código simples que mostra que DateFormat não é seguro para threads.

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

Como todos os threads estão usando o mesmo objeto SimpleDateFormat, ele lança a seguinte exceção.

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)

Mas se passarmos objetos diferentes para threads diferentes, o código será executado sem erros.

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

Estes são os resultados.

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

O OP perguntou por que isso acontece e o quê.
Adam

0

Isso causará ArrayIndexOutOfBoundsException

Além do resultado incorreto, ocorrerá uma falha de tempos em tempos. Depende da velocidade da sua máquina; no meu laptop, acontece uma vez a cada 100.000 chamadas, em média:

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

a última linha deve acionar a exceção do executor adiada:

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)
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.