3

I have many threads processing Trade objects where I use a RowMapper to map database columns to Trade object.

I understand SimpleDateFormat is not thread-safe in any Java. As a result, I get some unpredictable result in startDate. For example, I see date which is endDate also in startDate.

Here is my code:

public class ExampleTradeMapper {

    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd-MMM-yyyy");

    public void map(Trade trade, ResultSet rs, int rowNum) throws SQLException {    

        trade.setStartDate(getFormattedDate(rs.getDate("START_DATE")));
        trade.setEndDate(getFormattedDate(rs.getDate("END_DATE")));
        trade.setDescription(rs.getString("DESCRIPTION"));

    }

    private String getFormattedDate(Date date) {
        try {
            if (date != null)
                return DATE_FORMAT.format(date).toUpperCase();
            else
                return null;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

}


public class SomeRowMapper extends TradeMapper implements RowMapper<Trade> {

    @Override
    public Trade mapRow(ResultSet rs, int rowNum) throws SQLException {

        Trade trade = new Trade();

        map(trade, rs, rowNum);

        return trade;
    }
}

My core pool size is about 20 for this application with maximum about 50. These threads can be processing about 100s of trade records from the database at some time.

What would be the best way to make this date formatting thread safe? Should I be using a direct replacement using FastDateFormat?

Is there a better alternative way of doing making this thread safe?

Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
M06H
  • 1,675
  • 3
  • 36
  • 76
  • You could try instantiating `SimpleDateFormat` local to when you actually need the formatting. – meat Jul 16 '18 at 16:30
  • 1
    Per https://stackoverflow.com/questions/6840803/why-is-javas-simpledateformat-not-thread-safe you can choose to use DateTimeFormatter, or a single instance of SDF per thread. – Compass Jul 16 '18 at 16:30
  • I recommend you avoid the `SimpleDateFormat` class. It is not only long outdated, it is also notoriously troublesome. Today we have so much better in [`java.time`, the modern Java date and time API](https://docs.oracle.com/javase/tutorial/datetime/) and its `DateTomeFormatter`. Which BTW is thread-safe. – Ole V.V. Jul 16 '18 at 16:53
  • 1
    If `Trade` is your model class, why are your keeping dates as strings in it? General recommendation is to keep date objects, preferably `LocalDate` objects. Doing so would only move your problem to when you are presenting your data to the user, of course, but I found it worth asking. – Ole V.V. Jul 16 '18 at 16:59

3 Answers3

6

tl;dr

Rather than using strings, use java.time objects (LocalDate specifically) exchanged with your database via JDBC 4.2 or later.

myResultSet.getObject(      // Exchange modern java.time objects with your database.
    "START_DATE" ,
    LocalDate.class 
)                           // Returns a `LocalDate` object.
.format(                    // Generate a `String` representing textually the content of this `LocalDate`. 
    DateTimeFormatter.ofPattern( "dd-MMM-uuuu" , Locale.US )
)

23-Jan-2018

Being immutable objects, the java.time objects are thread-safe by design. You can cache the java.time objects for use across threads.

java.time

Making SimpleDateFormat thread safe

Don’t.

Use the modern java.time classes that years ago supplanted the troublesome old legacy date-time classes such as SimpleDateFormat, java.util.Date, java.sql.Date, and Calendar.

The java.time classes are designed to be thread-safe. They use immutable objects pattern, to return fresh objects based on the values of an original rather than “mutating” (altering) the original.

Use smart objects, not dumb strings

I see no reason for using strings in your example code: Not in your database access code, not in your business object (Trade).

JDBC

As of JDBC 4.2, we can exchange java.time objects with the database. For a database column of a type akin to the SQL-standard DATE, use the class LocalDate. The LocalDate class represents a date-only value without time-of-day and without time zone.

myPreparedStatement.setObject( … , myLocalDate ) ;

Retrieval.

LocalDate myLocalDate = myResultSet.getObject( … , LocalDate.class ) ;

Business object

Your Trade class should be holding member variables startDate & endDate as LocalDate objects, not strings.

public class Trade {
    private LocalDate startDate ;
    private LocalDate endDate ;
    … 

    // Getters
    public LocalDate getStartDate() { 
        return this.startDate ;
    }
    public LocalDate getEndDate() { 
        return this.endDate;
    }
    public Period getPeriod() {  // Number of years-months-days elapsed.
        return Period.between( this.startDate , this.endDate ) ;
    }

    // Setters
    public void setStartDate( LocalDate startDateArg ) { 
        this.startDate = startDateArg ;
    }
    public void setEndDate( LocalDate endDateArg ) { 
        this.endDate = endDateArg ;
    }

    @Override
    public toString() {
        "Trade={ " + "startDate=" + this.startDate.toString() …
    }
…
}

No need for strings, no need for formatting patterns.

Strings

To exchange or store date-time values as text, use the standard ISO 8601 formats rather than a custom format as seen in your Question.

The java.time classes use the ISO 8601 formats by default when parsing/generating strings. So no need to specify a formatting pattern.

LocalDate ld = LocalDate.parse( "2018-01-23" ) ; // January 23, 2018.
String s = ld.toString() ;  // Outputs 2018-01-23. 

For presentation in a user-interface, let java.time automatically localize. To localize, specify:

  • FormatStyle to determine how long or abbreviated should the string be.
  • Locale to determine:
    • The human language for translation of name of day, name of month, and such.
    • The cultural norms deciding issues of abbreviation, capitalization, punctuation, separators, and such.

Example:

Locale l = Locale.CANADA_FRENCH ; 
DateTimeFormatter f = 
    DateTimeFormatter.ofLocalizedDate( FormatStyle.FULL )
                     .withLocale( l ) ;
String output = ld.format( f ) ;

mardi 23 janvier 2018

The DateTimeFormatter class is thread-safe, by design, as an immutable object. You could hold one instance to be used across threads.


About java.time

The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.* classes.

Where to obtain the java.time classes?

The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
  • Thanks for the very useful explanation :). Since my `Trade` business object is of XML schema type annotated with `@XmlType`, the date fields must have format `dd-MMM-yyyy` in uppercase. Should I still have `Localdate` type in business object ? – M06H Jul 17 '18 at 14:27
  • 1
    I find it best to “tell the truth” in my programming. So yes, I’d keep a date-only value as a `LocalDate` type. Then bolt on whatever extra you need to make your XML framework happy. What XML framework are you using? Check to see if it has been updated to support java.time types. If not, can you tag a *getter* method returning that string as your XML property? And edit your Question to include these relevant facts. – Basil Bourque Jul 17 '18 at 14:58
  • 1
    maybe write an adapter class `@XmlJavaTypeAdapter(LocalDateAdapter.class)` and return formatted date in `marshall()` https://stackoverflow.com/questions/36156741/marshalling-localdate-using-jaxb – M06H Jul 17 '18 at 15:02
3

You can make it ThreadLocal. Every thread in the pool will hold their own formattor.

private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT = new ThreadLocal<SimpleDateFormat>() {
    @Override
    protected SimpleDateFormat initialValue() {
        return new SimpleDateFormat("dd-MMM-yyyy");
    }
};
xingbin
  • 27,410
  • 9
  • 53
  • 103
  • be aware of memory leaks when using thread locals and not cleaning them – white Jul 16 '18 at 16:54
  • @white *My core pool size is about 20 for this application with maximum about 50*... I think it will not cause memory leak. – xingbin Jul 16 '18 at 16:55
  • it is not only about SDF instances, you keep a link on entire classloader, https://plumbr.io/blog/locked-threads/how-to-shoot-yourself-in-foot-with-threadlocals f.ex. – white Jul 16 '18 at 16:57
0

Here you can see the fastest way to use date format in thread safe way. Because you have 3 ways to do it :

  1. With DateFormat.getDateInstance()
  2. With synchronized
  3. And the local thread way which from far offers the best performance

Full code sample :

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SimpleDateFormatThreadExample {

    private static String FORMAT = "dd-M-yyyy hh:mm:ss";

    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat(FORMAT);

    public static void main(String[] args) {

        final String dateStr = "02-1-2018 06:07:59";

        ExecutorService executorService = Executors.newFixedThreadPool(10);

        Runnable task = new Runnable() {

            @Override
            public void run() {
                parseDate(dateStr);
            }

        };

        Runnable taskInThread = new Runnable() {

            @Override
            public void run() {
                try {
                    ConcurrentDateFormatAccess concurrentDateFormatAccess = new ConcurrentDateFormatAccess();
                    System.out.println("Successfully Parsed Date " + concurrentDateFormatAccess.convertStringToDate(dateStr));
                    // don't forget to use CLEAN because the classloader with keep date format !
                    concurrentDateFormatAccess.clean();
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }

        };

        for (int i = 0; i < 100; i++) {
            executorService.submit(task);
            // remove this comment to use thread safe way !
            // executorService.submit(taskInThread);
        }
        executorService.shutdown();
    }

    private static void parseDate(String dateStr) {
        try {
            Date date = simpleDateFormat.parse(dateStr);
            System.out.println("Successfully Parsed Date " + date);
        } catch (ParseException e) {
            System.out.println("ParseError " + e.getMessage());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static class ConcurrentDateFormatAccess {

        private ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {

            @Override
            public DateFormat get() {
                return super.get();
            }

            @Override
            protected DateFormat initialValue() {
                return new SimpleDateFormat(FORMAT);
            }

            @Override
            public void remove() {
                super.remove();
            }

            @Override
            public void set(DateFormat value) {
                super.set(value);
            }

        };

        public void clean() {
            df.remove();
        }

        public Date convertStringToDate(String dateString) throws ParseException {
            return df.get().parse(dateString);
        }

    }

}
Zhar
  • 3,330
  • 2
  • 24
  • 25