“ JavaDateFormat 不是线程安全的”这会导致什么?

每个人都警告说 JavaDateFormat 不是线程安全的,我从理论上理解这个概念。

但是我无法想象我们会因此面临什么样的实际问题。比方说,我在一个类中有一个 DateFormat 字段,在多线程环境中类中的不同方法(格式化日期)中也使用了相同的字段。

这会不会导致:

  • 格式异常等任何异常
  • 数据不一致
  • 还有别的问题吗?

还有,请解释一下为什么。

64988 次浏览

粗略地说,你不应该把一个 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

正如 Jon Skeet 所指出的,在执行外部同步时,可以同时拥有静态和共享实例变量(即在对 DateFormat的调用中使用 synchronized)

日期格式不同步。建议为每个线程创建单独的格式实例。如果多个线程并发访问一种格式,则必须同步该格式 表面上。

这意味着假设你有一个 DateFormat 对象,你正在从两个不同的线程访问同一个对象,你正在调用这个对象的 format 方法,两个线程将同时在同一个对象上进入同一个方法,所以你可以想象它不会产生正确的结果

如果您必须以任何方式使用 DateFormat,那么您应该做一些事情

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

如果有多个线程操作/访问单个 DateFormat 实例并且没有使用同步,则可能会得到混乱的结果。这是因为多个非原子操作可能会改变状态或不一致地查看内存。

我预计数据会被破坏——例如,如果您同时解析两个日期,您可能会让一个调用受到来自另一个调用的数据的污染。

很容易想象这种情况是如何发生的: 解析通常涉及维护到目前为止所阅读内容的一定数量的状态。如果两个线程都践踏同一状态,就会出现问题。例如,DateFormat公开类型为 Calendarcalendar字段,查看 SimpleDateFormat的代码,一些方法调用 calendar.set(...),另一些方法调用 calendar.get(...)。这显然不是线程安全的。

我还没有研究为什么 DateFormat不是线程安全的 一模一样细节,但是对于我来说,知道没有同步的 是不安全的就足够了——不安全的确切方式甚至可能在不同版本之间发生变化。

个人而言,我会使用来自 乔达时间的解析器,因为它们是 线程安全的——而 Joda Time 是一个更好的日期和时间 API:)

数据已损坏。昨天,我在我的多线程程序中注意到了这个问题,在这个程序中我有一个静态的 DateFormat对象,并调用它的 format()来获取通过 JDBC 读取的值。我有 SQL 选择语句,其中我用不同的名称读取相同的日期(SELECT date_from, date_from AS date_from1 ...)。这样的语句在5个线程中用于 WHERE类中的不同日期。日期看起来“正常”,但它们的价值不同——而所有日期都来自同一年,只是月份和日期发生了变化。

其他的答案告诉你如何避免这种腐败。我使我的 DateFormat不是静态的,现在它是调用 SQL 语句的类的一个成员。我还测试了静态版本的同步。两者都运行良好,性能没有差异。

Format、 NumberFormat、 DateFormat、 MessageFormat 等规范的设计并不是线程安全的。此外,解析方法调用 Calendar.clone()方法,它会影响日历占用,因此许多线程并发解析将改变 Calendar 实例的克隆。

对于更多的错误报告,如 这个这个,其结果是 DateFormat 线程安全问题。

我们来试试。

下面是一个多线程使用共享 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;
}
}

这里是一个良好的 邮寄与更多的细节。

如果您正在使用 Java8,那么您可以使用 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 不是线程安全的。

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

在最佳答案中,Dogbane 给出了一个使用 parse函数的例子及其导致的结果。下面的代码可以让您检查 format函数。

注意,如果你改变执行器(并发线程)的数量,你会得到不同的结果:

  • newFixedThreadPool设置为5,循环每次都会失败。
  • 如果设置为1,循环将始终正常工作(显然,所有任务实际上都是逐个运行的)
  • 设置为2,循环只有6% 的工作机会。

我猜 YMMV 取决于你的处理器。

format函数由于格式化来自另一个线程的时间而失败。这是因为内部 format函数使用的是在 format函数开始时设置的 calendar对象。而 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");
}

这会导致 ArrayIndexOutOfBoundsException

除了不正确的结果,它还会让你时不时地崩溃。这取决于你的机器的速度; 在我的笔记本电脑上,这种情况平均每10万次通话中才会发生一次:

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)