Reputation:
I want to show to a colleague that SimpleDateFormat is not thread-safe through a simple JUnit test. The following class fails to make my point (reusing SimpleDateFormat in a multi-threaded environment) and I don't understand why. Can you spot what is preventing my use of SDF from throwing a runtime exception?
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";
}
}
}
Upvotes: 5
Views: 1636
Reputation: 229461
Lack of thread safety doesn't necessarily mean that the code will throw an exception. This was explained in Andy Grove's article, SimpleDateFormat and Thread Safety, which is no longer available online. In it, he showed SimpleDateFormat
's lack of thread safety by showing that the output would not always be correct, given different inputs.
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!
While the original article is no longer available online, the following code illustrates the issue. It was created based on articles that appeared to have been based on Andy Grove's initial article.
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");
}
}
}
Sometimes this conversion fails by returning the wrong date, and sometimes it fails with a NumberFormatException
:
java.lang.NumberFormatException: For input string: ".E2.31E2"
Upvotes: 13
Reputation: 7328
It is not thread safe because of this code in SimpleDateFormat
(in 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);
....
}
Each call to format stores the date in a calendar
member variable of the SimpleDateFormat
, and then subsequently applies the formatting to the contents of the calendar
variable (not the date
parameter).
So, as each call to format happens the data for all currently running formats may change (depending on the coherence model of your architecture) the data in the calendar
member variable that is used by every other thread.
So if you run multiple concurrent calls to format you may not get an exception, but each call may return a result derived from the date of one of the other calls to format - or a hybrid combination of data from many different calls to format.
Upvotes: 3
Reputation: 14363
Isn't this part from javadoc of SimpleDateFormatter
has sufficent proof about it?
Synchronization
Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.
And the major observation of not being thread safe is to get unexpected results and not an exception.
Upvotes: 4