Все предупреждают, что Java DateFormat не является потокобезопасным, и я теоретически понимаю эту концепцию.
Но я не могу представить себе, с какими реальными проблемами мы можем столкнуться из-за этого. Скажем, у меня есть поле DateFormat в классе, и оно используется в разных методах этого класса (форматирование дат) в многопоточной среде.
Это вызовет:
- любое исключение, такое как исключение формата
- расхождение в данных
- любая другая проблема?
Также объясните, почему.
java
multithreading
date-format
haps10
источник
источник
Ответы:
Давай попробуем.
Вот программа, в которой несколько потоков используют общий
SimpleDateFormat
.Программа :
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()); } }
Выполните это несколько раз, и вы увидите:
Исключения :
Вот несколько примеров:
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)
Неправильные результаты :
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
Правильные результаты :
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 в многопоточной среде - использовать
ThreadLocal
переменную для храненияDateFormat
объекта, что означает, что каждый поток будет иметь свою собственную копию и не должен ждать, пока другие потоки освободят ее. Вот как: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; } }
Вот хороший пост с более подробной информацией.
источник
Я бы ожидал повреждения данных - например, если вы анализируете две даты одновременно, у вас может быть один вызов, загрязненный данными из другого.
Легко представить, как это могло произойти: синтаксический анализ часто включает в себя поддержание определенного состояния того, что вы прочитали. Если два потока попирают одно и то же состояние, возникнут проблемы. Например,
DateFormat
предоставляетcalendar
поле типаCalendar
и, глядя на кодSimpleDateFormat
, вызывает вызов некоторых методов и вызовcalendar.set(...)
другихcalendar.get(...)
. Это явно небезопасно.Я не смотрел в точные подробности того , почему
DateFormat
не поточно-, но для меня этого достаточно , чтобы знать , что это небезопасно без синхронизации - точные манеры несохранность даже изменения между версиями.Лично я хотел бы использовать парсер из Joda времени вместо того, чтобы , как они являются поточно - и Joda время гораздо лучше , дата и времени API для начала :)
источник
Если вы используете Java 8, вы можете использовать
DateTimeFormatter
.Код:
LocalDate date = LocalDate.now(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); String text = date.format(formatter); System.out.println(text);
Выход:
2017-04-17
источник
Грубо говоря, вы не должны определять
DateFormat
переменную экземпляра объекта, к которому обращаются многие потоки, илиstatic
.Итак, если к вам
Foo.handleBar(..)
обращаются несколько потоков, вместо:public class Foo { private DateFormat df = new SimpleDateFormat("dd/mm/yyyy"); public void handleBar(Bar bar) { bar.setFormattedDate(df.format(bar.getStringDate()); } }
вы должны использовать:
public class Foo { public void handleBar(Bar bar) { DateFormat df = new SimpleDateFormat("dd/mm/yyyy"); bar.setFormattedDate(df.format(bar.getStringDate()); } }
Кроме того, во всех случаях нет
static
DateFormat
Как отметил Джон Скит, у вас могут быть как статические, так и общие переменные экземпляра, если вы выполняете внешнюю синхронизацию (т.е. используете
synchronized
вокруг вызовов кDateFormat
)источник
SimpleDateFormat
очень частое создание нового . Это будет зависеть от модели использования.Это означает, что предположим, что у вас есть объект DateFormat, и вы обращаетесь к одному и тому же объекту из двух разных потоков, и вы вызываете метод форматирования для этого объекта, оба потока будут вводить один и тот же метод в одно и то же время для одного и того же объекта, поэтому вы можете визуализировать его не приведет к правильному результату
Если вам нужно как-то работать с DateFormat, вам следует что-то сделать
public synchronized myFormat(){ // call here actual format method }
источник
В лучшем ответе dogbane привел пример использования
parse
функции и к чему она приводит. Ниже приведен код, который позволяет вам проверитьformat
функцию.Обратите внимание, что если вы измените количество исполнителей (параллельных потоков), вы получите разные результаты. Из моих экспериментов:
newFixedThreadPool
значение 5, и цикл каждый раз будет терпеть неудачу.Я предполагаю YMMV в зависимости от вашего процессора.
format
Функция не по времени форматирования из другого потока. Это связано с тем, что внутренняяformat
функция используетcalendar
объект, который устанавливается в началеformat
функции. Аcalendar
объект - это свойствоSimpleDateFormat
класса. Вздох.../** * 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"); }
источник
Данные повреждены. Вчера я заметил это в своей многопоточной программе, где у меня был статический
DateFormat
объект, и вызвал егоformat()
для значений, прочитанных через JDBC. У меня был оператор выбора SQL, в котором я читал одну и ту же дату с разными именами (SELECT date_from, date_from AS date_from1 ...
). Такие высказывания использовались в 5 потоках для разных дат вWHERE
классе. Даты выглядели «нормально», но различались по значению - в то время как все даты относились к одному году, изменились только месяц и день.Другие ответы показывают, как избежать такой коррупции. Я сделал свой
DateFormat
не static, теперь он является членом класса, который вызывает операторы SQL. Я тестировал также статическую версию с синхронизацией. Оба работали хорошо, без разницы в производительности.источник
Спецификации Format, NumberFormat, DateFormat, MessageFormat и т. Д. Не предназначены для обеспечения многопоточности. Кроме того, метод синтаксического анализа вызывает
Calendar.clone()
метод и влияет на следы календаря, поэтому одновременный синтаксический анализ многих потоков изменит клонирование экземпляра Calendar.Более того, это отчеты об ошибках, такие как этот и этот , с результатами проблемы безопасности потока DateFormat.
источник
Если есть несколько потоков, управляющих / обращающихся к одному экземпляру DateFormat, а синхронизация не используется, можно получить зашифрованные результаты. Это связано с тем, что несколько неатомарных операций могут непоследовательно изменять состояние или видеть память.
источник
Это мой простой код, который показывает, что DateFormat не является потокобезопасным.
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(); } }
Поскольку все потоки используют один и тот же объект SimpleDateFormat, он вызывает следующее исключение.
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)
Но если мы передаем разные объекты в разные потоки, код работает без ошибок.
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(); } }
Вот результаты.
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
источник
Это вызовет
ArrayIndexOutOfBoundsException
Помимо неверного результата, время от времени он будет вызывать сбой. Это зависит от скорости вашей машины; в моем ноутбуке это происходит в среднем один раз на 100 000 звонков:
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();
последняя строка может вызвать исключение отложенного исполнителя:
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)
источник