5

我想通过一个简单的 JUnit 测试向同事展示 SimpleDateFormat不是线程安全的。以下课程未能说明我的观点(在多线程环境中重用 SimpleDateFormat),我不明白为什么。你能发现是什么阻止我使用 SDF 引发运行时异常吗?

public class SimpleDateFormatThreadTest
{
    @Test
    public void test_SimpleDateFormat_MultiThreaded() throws ParseException{
        Date aDate = (new SimpleDateFormat("dd/MM/yyyy").parse("31/12/1999"));
        DataFormatter callable = new DataFormatter(aDate);

        ExecutorService executor = Executors.newFixedThreadPool(1000);
        Collection<DataFormatter> callables = Collections.nCopies(1000, callable);

        try{
            List<Future<String>> futures = executor.invokeAll(callables);
            for (Future f : futures){
                try{
                    assertEquals("31/12/1999", (String) f.get());
                }
                catch (ExecutionException e){
                    e.printStackTrace();
                }
            }
        }
        catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

class DataFormatter implements Callable<String>{
    static SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");

    Date date;

    DataFormatter(Date date){
        this.date = date;
    }

    @Override
    public String call() throws RuntimeException{
        try{
            return sdf.format(date);
        }
        catch (RuntimeException e){
            e.printStackTrace();
            return "EXCEPTION";
        }
    }
}
4

3 回答 3

13

缺乏线程安全并不一定意味着代码会抛出异常。Andy Grove 的文章SimpleDateFormat 和 Thread Safety对此进行了解释,该文章不再在线提供。在其中,他SimpleDateFormat通过表明在给定不同输入的情况下输出并不总是正确的,从而表明缺乏线程安全性。

当我运行此代码时,我得到以下输出:

    java.lang.RuntimeException: date conversion failed after 3 iterations.
    Expected 14-Feb-2001 but got 01-Dec-2007

请注意,“01-Dec-2007”甚至不是测试数据中的字符串之一。它实际上是其他两个线程正在处理的日期的组合!

虽然原始文章不再在线提供,但以下代码说明了该问题。它是根据似乎是基于 Andy Grove 最初文章的文章创建的。

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;

public class SimpleDateFormatThreadSafety {
    private final SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MMM-yyyy", Locale.US);

    public static void main(String[] args) {
        new SimpleDateFormatThreadSafety().dateTest(List.of("01-Jan-1999", "14-Feb-2001", "31-Dec-2007"));
    }

    public void dateTest(List<String> testData) {
        testData.stream()
                .map(d -> new Thread(() -> repeatedlyParseAndFormat(d)))
                .forEach(Thread::start);
    }

    private void repeatedlyParseAndFormat(String value) {
        for (int i = 0; i < 1000; i++) {
            Date d = tryParse(value);
            String formatted = dateFormat.format(d);
            if (!value.equals(formatted)) {
                throw new RuntimeException("date conversion failed after " + i
                        + " iterations. Expected " + value + " but got " + formatted);
            }
        }
    }

    private Date tryParse(String value) {
        try {
            return dateFormat.parse(value);
        } catch (ParseException e) {
            throw new RuntimeException("parse failed");
        }
    }
}

有时此转换会因返回错误的日期而失败,有时会因以下原因而失败NumberFormatException

java.lang.NumberFormatException: For input string: ".E2.31E2"
于 2012-09-21T15:19:07.960 回答
4

javadoc的SimpleDateFormatter这部分不是有足够的证据吗?

同步

日期格式不同步。建议为每个线程创建单独的格式实例。如果多个线程同时访问一个格式,它必须在外部同步。

不是线程安全的主要观察是得到意想不到的结果,而不是异常。

于 2012-09-21T15:20:11.823 回答
3

SimpleDateFormat由于(在 sun JVM 1.7.0_02 中)中的以下代码,它不是线程安全的:

private StringBuffer format(Date date, StringBuffer toAppendTo,
                            FieldDelegate delegate) {
    // Convert input date to time field list
    calendar.setTime(date);
    ....
}

对 format 的每次调用都将日期存储在 的calendar成员变量中SimpleDateFormat,然后随后将格式应用于calendar变量的内容(而不是date参数)。

因此,当每次调用 format 时,所有当前运行的格式的数据都可能会改变(取决于架构的一致性模型)calendar每个其他线程使用的成员变量中的数据。

因此,如果您对 format 运行多个并发调用,您可能不会遇到异常,但每个调用都可能返回从其他调用 format 的日期派生的结果 - 或来自许多不同调用 format 的数据的混合组合。

于 2012-09-21T15:27:44.210 回答