0

I have tried to write an example to show SimpleDateFormat is thread unsafe. but it does not work! Can anyone give me an example of showing SimpleDateFormat is thread unsafe?

public static void main(String[] args) throws ParseException, InterruptedException {

    Date aDate = (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2016-12-15 23:59:59"));

    ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 1000);

    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    DataFormatter callable = new DataFormatter(sdf, aDate);
    Collection<DataFormatter> callables = Collections.nCopies(1000, callable);
    executor.invokeAll(callables);
    executor.shutdown();
}

private static class DataFormatter implements Callable<String> {

    private SimpleDateFormat sdf;
    private Date aDate;

    public DataFormatter(SimpleDateFormat sdf, Date aDate) {
        this.sdf = sdf;
        this.aDate = aDate;
    }

    @Override
    public String call() throws Exception {
        String format = sdf.format(aDate);
        Assert.assertEquals("2016-12-15 23:59:59", format);
        return format;
    }
}
Kay
  • 59
  • 7
  • 1
    You can't really deterministically show that something is not thread safe. It's not the sort of thing that can be really tested. – Louis Wasserman Dec 15 '16 at 06:49
  • I wrote a program in two threads using a shared `SimpleDateFormat` instance and formatting different dates. When I run it on my computer, one of the threads get an incorrect result in the first call to `format()`. After a couple of hundred calls from each thread, an `ArrayIndexOutOfBoundsException` happens in the `Calendar` class (`SimpleDateFormat` is using `Calendar`). – Ole V.V. Dec 15 '16 at 09:49
  • [This answer](http://stackoverflow.com/a/24794704/5772882) seems to be doing something similar to what I did. I didn’t try that program out, though. – Ole V.V. Dec 15 '16 at 09:51

2 Answers2

2

Can anyone give me an example of showing SimpleDateFormat is thread unsafe?

Sure. The problem with your code is that you are trying to format the same date over and over again so the shared fields are never holding different values. If we look at the code from SimpleDateFormat we see that it extends DateFormat which has a shared Calendar calendar field. That's the reentrance problem with the class.

// shared with everyone else calling the same SimpleDateFormat
protected Calendar calendar;
...
// method from DateFormat that is extended by SimpleDateFormat
private StringBuffer format(Date date, StringBuffer toAppendTo, FieldDelegate delegate) {
    calendar.setTime(date);
    ...

As an aside, also notice that it uses StringBuffer which means that this uses synchronized methods. It's depressing that we pay for synchronization performance penalties but we don't get reentrance with SimpleDateFormat.

Here's my take on how to demonstrate it. I'm just running the date format twice on a random date and examining the results. It fails immediately with just 20 concurrent threads.

public class SimpleDateFormatEnosafe {

    private static final SimpleDateFormat format =
            new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        DataFormatter formatter = new DataFormatter();
        for (int i = 0; i < 20; i++) {
            executor.submit(formatter);
        }
        executor.shutdown();
        // NOTE: this could never finish if all but one thread fails in the pool
    }

    private static class DataFormatter implements Runnable {
        @Override
        public void run() {
            ThreadLocalRandom random = ThreadLocalRandom.current();
            while (true) {
                Date date = new Date(random.nextLong());
                String output1 = format.format(date);
                String output2 = format.format(date);
                if (!output1.equals(output2)) {
                    System.out.println(output1 + " != " + output2);
                    break;
                }
            }
        }
    }
}
Gray
  • 115,027
  • 24
  • 293
  • 354
0

I tried, and it turned out to be easy. I have two threads formatting different dates using a shared SimpleDateFormat instance (declared DateFormat, but the actual instance is of class SimpleDateFormat). Here is my code:

public class FormattingThread extends Thread {

    private final static DateFormat shared = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL, Locale.GERMANY);
    private final static ZoneOffset offset = ZoneOffset.ofHours(5);

    LocalDateTime date;

    public FormattingThread(LocalDateTime date) {
        super("FormattingThread");
        this.date = date;
    }

    @Override
    public void run() {
        final DateFormat myOwn = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL, Locale.GERMANY);
        int stepCount = 0;
        while (true) {
            stepCount++;
            Date classical = Date.from(date.toInstant(offset));
            String formatted = myOwn.format(classical);
            String formattedThreadUnsafe = shared.format(classical);
            if (! formatted.equals(formattedThreadUnsafe)) {
                System.err.println("Mine " + formatted + "; shared " + formattedThreadUnsafe
                        + " in " + stepCount + " steps");
            }
            date = date.plusDays(23);
        }
    }

    public static void main(String[] args) {
        new FormattingThread(LocalDateTime.now()).start();
        new FormattingThread(LocalDateTime.now().plusHours(17)).start();
    }

}

On my computer, one of the threads gets an incorrect result from its first call to format() . After a couple of hundred calls from each thread an ArrayIndexOutOfBoundsException occurs in the Calendar class (SimpleDateFormat uses Calendar).

I still want to stress as has been said already: that a class is not threadsafe, does not guarantee that you can make an error happen when using it from more than one thread. The stories of synchronization errors that didn't reveal themselves in thorough tests and then surfaced in production, are very many. I recently had one in a Java program that didn't happen on Mac and Linux, but a Windows user reported it and we had to release a bug fix version quickly. Such incidents are quite expensive.

Ole V.V.
  • 81,772
  • 15
  • 137
  • 161