"Java DateFormat не является потокобезопасным", к чему это приводит?
Все предупреждают, что Java DateFormat не является поточно-ориентированным, и я теоретически понимаю эту концепцию.
Но я не могу представить, с какими реальными проблемами мы можем столкнуться из-за этого. Скажем, у меня есть поле DateFormat в классе, и оно используется в разных методах класса (форматирование дат) в многопоточной среде.
Будет ли это причиной:
- любое исключение, такое как форматное исключение
- расхождение в данных
- любая другая проблема?
Также, пожалуйста, объясните почему.
10 ответов
Давайте попробуем это.
Вот программа, в которой несколько потоков используют общий 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 Time, так как они потокобезопасны - а Joda Time - намного лучший 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
)
Форматы даты не синхронизированы. Рекомендуется создавать отдельные экземпляры формата для каждого потока. Если несколько потоков обращаются к формату одновременно, он должен быть синхронизирован извне.
Это означает, что у вас есть объект DateFormat, и вы обращаетесь к одному и тому же объекту из двух разных потоков, и вы вызываете метод форматирования для этого объекта, и оба потока будут одновременно входить в один и тот же метод для одного и того же объекта, чтобы вы могли визуализировать его выигрыш не приводит к правильному результату
Если вам нужно работать с DateFormat каким-либо образом, то вы должны сделать что-то
public synchronized myFormat(){
// call here actual format method
}
В лучшем ответе собачья баня привела пример использования parse
функция и к чему это приводит. Ниже приведен код, который позволяет вам проверить format
функция.
Обратите внимание, что если вы измените количество исполнителей (одновременных потоков), вы получите разные результаты. Из моих экспериментов:
- Покидать
newFixedThreadPool
установите на 5, и цикл будет каждый раз сбой. - Установите 1, и цикл всегда будет работать (очевидно, поскольку все задачи выполняются одна за другой)
- Установите значение 2, и вероятность того, что цикл будет работать, составляет всего около 6%.
Я предполагаю, что 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
clasue. Даты выглядели "нормальными", но они различались по значению - в то время как все даты были из того же года, менялись только месяц и день.
Другие ответы показывают, как избежать такой коррупции. Я сделал мой DateFormat
не статический, теперь он является членом класса, который вызывает операторы SQL. Я также тестировал статическую версию с синхронизацией. Оба работали хорошо, без разницы в производительности.
Спецификации Format, NumberFormat, DateFormat, MessageFormat и т. Д. Не предназначены для обеспечения многопоточности. Также метод parse вызывает Calendar.clone()
метод, и это влияет на следы календаря, поэтому много потоков, одновременно анализирующих, изменит клонирование экземпляра Calendar.
Более того, это отчеты об ошибках, такие как эта и эта, с результатами проблемы безопасности потоков 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)
Если есть несколько потоков, которые манипулируют / обращаются к одному экземпляру DateFormat и синхронизация не используется, возможно получить зашифрованные результаты. Это связано с тем, что несколько неатомарных операций могут изменять состояние или отображать память непоследовательно.