证明SimpleDateFormat不是线程安全的

5

我想通过一个简单的JUnit测试向同事展示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";
        }
    }
}

1
你应该告诉你的同事阅读Javadoc,这样你们就可以继续进行一些正式的工作了! - David Grant
是的,SimpleDateFormat 不是线程安全的。我们(不好的)经验表明,它不会抛出异常,而是执行疯狂的格式化(实际上更糟糕)。尝试在您的代码中输出值。 - Bruno Grieder
为每个线程创建另一个SDF,并以两种方式格式化它。然后比较输出,如果输出不匹配,则抛出异常。 - Stefan
2
唯一证明代码是否线程安全的方法就是阅读代码。通过运行单元测试无法确保您已触发了所有可能的错误。 - Peter Lawrey
当然可以。我很惊讶有人会要求证明这样的事情。如果你喜欢这样的事情,我想我可以写一个测试来尝试强制它出现故障。每个人都需要一项爱好。通过测试可以证明某些东西是线程不安全的,虽然很困难:你可以演示并消除所有线程安全的元素,只留下一种可能性。但我认为我们更关心的是证明线程安全,这是无法通过测试来完成的。 - fool4jesus
线程安全问题本质上是不确定的。你可以测试某个东西一万次而没有看到任何问题,但在第一万零一次尝试时它可能会失败。如果某些代码没有明确说明它是线程安全的,那么你必须假设它不是线程安全的。 - Ian Roberts
3个回答

13
缺乏线程安全并不一定意味着代码会抛出异常。这在Andy Grove的文章SimpleDateFormat and Thread Safety中有所解释,该文章已不再在线上提供。他展示了SimpleDateFormat缺乏线程安全性,并展示了给定不同输入时输出不总是正确的情况。

When I run this code, I get the following output:

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

Note that "01-Dec-2007" isn't even one of the strings in the test data. It is actually a combination of the dates being processed by the other two threads!

虽然原始文章已不再在线上提供,但以下代码展示了所述问题。它是基于一些文章创建的,这些文章似乎是基于安迪·格罗夫的最初文章。
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"

它可能会抛出异常,但它们将被伪装成奇怪的解析异常,就像这里讨论的那个一样。 - Brian
我阅读了这篇文章并相应地更新了我的代码。但是在我的JUnit上仍然看不到这种行为。这是我对多线程有什么误解吗? - user689842
@Pomario:也许可以尝试同时给它不同的日期格式? - Claudiu
@Pomario,也许你应该尝试切换到另一种拥有更多CPU/核心来运行测试的架构。 - Gray
很遗憾,该链接现在已经失效。 - M. Justin
由于链接已经失效,我从各种文章中(大多数是我不会说的语言)拼凑出了我认为原始代码最初的功能,并且这些文章似乎是基于原始博客文章的。我根据这个代码创建了更新后的答案。 - M. Justin

4

这段话来自SimpleDateFormatter的javadoc,它不足以证明它的线程安全性。

同步

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

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


3

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

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

每次调用format都会将日期存储在SimpleDateFormat的calendar成员变量中,然后随后将格式应用于calendar变量的内容(而不是date参数)。
因此,每次对format的调用会更改所有当前运行格式的数据(取决于你的架构的一致性模型),这些数据是由其他线程使用的calendar成员变量中的数据。
因此,如果同时运行多个并发调用format,则可能不会出现异常,但每个调用可能返回从另一个调用到format之一的日期派生的结果 - 或多个不同的调用format数据的混合组合。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接