"Java DateFormat no es seguro para subprocesos" ¿a qué conduce esto?


143

Todo el mundo advierte que Java DateFormat no es seguro para subprocesos y entiendo el concepto teóricamente.

Pero no puedo visualizar qué problemas reales podemos enfrentar debido a esto. Digamos, tengo un campo DateFormat en una clase y el mismo se usa en diferentes métodos en la clase (fechas de formato) en un entorno de subprocesos múltiples.

¿Esto causará:

  • cualquier excepción como excepción de formato
  • discrepancia en los datos
  • cualquier otro problema?

Además, explique por qué.


1
Esto es a lo que conduce: stackoverflow.com/questions/14309607/…
caw

Es 2020 ahora. Al ejecutar mis pruebas (en paralelo) descubrí que una fecha de un hilo se devuelve casualmente cuando otro hilo intenta formatear una fecha. Me tomó un par de semanas investigar de qué depende, hasta que descubrí en un formateador que un constructor crea una instancia de un calendario, y luego el calendario se configura para tomar la fecha en que formateamos. ¿Sigue siendo 1990 en sus cabezas? Quién sabe.
Vlad Patryshev

Respuestas:


263

Probémoslo.

Aquí hay un programa en el que múltiples hilos usan un compartido 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());
    }
}

Ejecute esto varias veces y verá:

Excepciones :

Aquí están algunos ejemplos:

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 incorrectos :

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 correctos :

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

Otro enfoque para usar de forma segura DateFormats en un entorno de subprocesos múltiples es usar una ThreadLocalvariable para contener el DateFormat objeto, lo que significa que cada subproceso tendrá su propia copia y no necesita esperar a que otros subprocesos lo liberen. Así es como:

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

Aquí hay una buena publicación con más detalles.


1
Me encanta esta respuesta :-)
Sundararaj Govindasamy

Creo que la razón por la que esto es tan frustrante para los desarrolladores es que, a primera vista, parece que debería ser una llamada de función 'funcionalmente orientada'. Por ejemplo, para la misma entrada, espero la misma salida (incluso si varios hilos lo llaman). Creo que la respuesta se reduce a que los desarrolladores de Java no aprecian FOP en el momento en que escribieron la lógica de fecha y hora original. Así que al final, simplemente decimos "no hay razón para que sea así, sino que simplemente está mal".
Lezorte

30

Esperaría la corrupción de datos, por ejemplo, si analiza dos fechas al mismo tiempo, podría tener una llamada contaminada por datos de otra.

Es fácil imaginar cómo podría suceder esto: el análisis a menudo implica mantener una cierta cantidad de estado en cuanto a lo que has leído hasta ahora. Si dos hilos se pisotean en el mismo estado, obtendrá problemas. Por ejemplo, DateFormatexpone un calendarcampo de tipo Calendary, mirando el código de SimpleDateFormat, algunos métodos llaman calendar.set(...)y otros llaman calendar.get(...). Esto claramente no es seguro para subprocesos.

No he examinado los detalles exactos de por qué DateFormatno es seguro para subprocesos, pero para mí es suficiente saber que no es seguro sin sincronización: las formas exactas de no seguridad incluso podrían cambiar entre lanzamientos.

Personalmente, usaría los analizadores de Joda Time , ya que son seguros para subprocesos, y Joda Time es una API de fecha y hora mucho mejor para comenzar :)


1
+1 jodatime y sonar para hacer cumplir su uso: mestachs.wordpress.com/2012/03/17/…
mestachs

18

Si está utilizando Java 8, puede usarlo DateTimeFormatter.

Un formateador creado a partir de un patrón puede usarse tantas veces como sea necesario, es inmutable y es seguro para subprocesos.

Código:

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

Salida:

2017-04-17

10

Aproximadamente, que no debe definir una DateFormatvariable de instancia de un objeto al que acceden muchos hilos, o static.

Los formatos de fecha no están sincronizados. Se recomienda crear instancias de formato separadas para cada hilo.

Por lo tanto, en caso Foo.handleBar(..)de que varios subprocesos accedan a su, en lugar de:

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

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

Deberías usar:

public class Foo {

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

Además, en todos los casos, no tengo un static DateFormat

Como señaló Jon Skeet, puede tener variables de instancia tanto estáticas como compartidas en caso de que realice una sincronización externa (es decir, use synchronizedalrededor de llamadas al DateFormat)


2
No veo que eso siga en absoluto. No hago que la mayoría de mis tipos sean seguros para subprocesos, por lo que tampoco espero que sus variables de instancia sean seguras para subprocesos, necesariamente. Es más razonable decir que no debe almacenar un DateFormat en una variable estática , o si lo hace, necesitará sincronización.
Jon Skeet

1
Eso es generalmente mejor - aunque sería bien tener un DateFormat estática si lo hizo de sincronización. Eso puede funcionar mejor en muchos casos que crear uno nuevo con SimpleDateFormatmucha frecuencia. Dependerá del patrón de uso.
Jon Skeet

1
¿Podría explicar cómo y por qué la instancia estática puede causar problemas en un entorno multiproceso?
Alexandr

44
porque almacena cálculos intermedios en variables de instancia, y eso no es seguro
subprocesos

2

Los formatos de fecha no están sincronizados. Se recomienda crear instancias de formato separadas para cada hilo. Si varios subprocesos acceden a un formato simultáneamente, debe sincronizarse externamente.

Esto significa que suponga que tiene un objeto de DateFormat y está accediendo al mismo objeto desde dos hilos diferentes y está llamando al método de formato sobre ese objeto, ambos hilos ingresarán en el mismo método al mismo tiempo en el mismo objeto para que pueda visualizarlo ganado no resulte en el resultado adecuado

Si tiene que trabajar con DateFormat de alguna manera, entonces debe hacer algo

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

1

Los datos están dañados. Ayer lo noté en mi programa multiproceso donde tenía un DateFormatobjeto estático y pedí que format()leyeran los valores a través de JDBC. Tenía una instrucción de selección SQL donde leía la misma fecha con diferentes nombres ( SELECT date_from, date_from AS date_from1 ...). Dichas declaraciones fueron utilizadas en 5 hilos para varias fechas en WHEREclase. Las fechas parecían "normales" pero tenían un valor diferente, mientras que todas las fechas eran del mismo año, solo el mes y el día cambiaron.

Otras respuestas le muestran el camino para evitar tal corrupción. Hice mi DateFormatno estático, ahora es miembro de una clase que llama a las declaraciones SQL. También probé la versión estática con sincronización. Ambos funcionaron bien sin diferencias en el rendimiento.


1

Las especificaciones de Format, NumberFormat, DateFormat, MessageFormat, etc. no fueron diseñadas para ser seguras para subprocesos. Además, el método de análisis llama al Calendar.clone()método y afecta las huellas del calendario, por lo que muchos subprocesos que analizan simultáneamente cambiarán la clonación de la instancia del calendario.

Para obtener más información, estos son informes de errores como este y este , con resultados del problema de seguridad de subprocesos de DateFormat.


1

En la mejor respuesta, dogbane dio un ejemplo del uso de la parsefunción y a lo que conduce. A continuación hay un código que le permite verificar la formatfunción.

Tenga en cuenta que si cambia el número de ejecutores (hilos concurrentes) obtendrá resultados diferentes. De mis experimentos:

  • Déjelo newFixedThreadPoolen 5 y el bucle fallará siempre.
  • Establezca en 1 y el bucle siempre funcionará (obviamente, ya que todas las tareas se ejecutan una por una)
  • Se establece en 2 y el bucle tiene solo un 6% de posibilidades de funcionar.

Supongo que YMMV depende de tu procesador.

La formatfunción falla formateando el tiempo desde un hilo diferente. Esto se debe a que internamente la formatfunción está utilizando un calendarobjeto que se configura al comienzo de la formatfunción. Y el calendarobjeto es una propiedad de la SimpleDateFormatclase. 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

Si hay múltiples hilos manipulando / accediendo a una sola instancia de DateFormat y la sincronización no se usa, es posible obtener resultados codificados. Esto se debe a que varias operaciones no atómicas podrían estar cambiando de estado o viendo la memoria de manera inconsistente.


0

Este es mi código simple que muestra que DateFormat no es seguro para subprocesos.

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 los hilos están usando el mismo objeto SimpleDateFormat, arroja la siguiente excepción.

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)

Pero si pasamos diferentes objetos a diferentes hilos, el código se ejecuta sin errores.

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

Estos son los 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

El OP preguntó por qué sucede esto y qué.
Adam

0

Esto causará ArrayIndexOutOfBoundsException

Además del resultado incorrecto, ocasionará un bloqueo de vez en cuando. Depende de la velocidad de su máquina; en mi laptop, ocurre una vez en 100,000 llamadas en promedio:

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

la última línea podría desencadenar la excepción del ejecutor pospuesto:

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)
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.