使 SimpleDateFormat 线程安全

Making SimpleDateFormat thread safe

我有很多线程处理 Trade 对象,其中我使用 RowMapper 将数据库列映射到 Trade 对象。

我了解 SimpleDateFormat 在任何 Java 中都不是线程安全的。结果,我在 startDate 中得到了一些不可预测的结果。例如,我在 startDate 中也看到日期 endDate

这是我的代码:

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;
    }
}

对于此应用程序,我的核心池大小约为 20,最大约为 50。这些线程有时可以处理来自数据库的大约 100 条交易记录。

确保此日期格式化线程安全的最佳方法是什么?我应该使用 FastDateFormat 直接替换吗?

是否有更好的替代方法来确保此线程安全?

你能做到ThreadLocal。池中的每个线程都将拥有自己的格式化程序。

private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT = new ThreadLocal<SimpleDateFormat>() {
    @Override
    protected SimpleDateFormat initialValue() {
        return new SimpleDateFormat("dd-MMM-yyyy");
    }
};

tl;博士

不使用字符串,而是使用通过 JDBC 4.2 或更高版本与数据库交换的 java.time 对象(特别是 LocalDate)。

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

作为不可变对象,java.time 对象在设计上是线程安全的。您可以缓存 java.time 对象以供跨线程使用。

java.time

Making SimpleDateFormat thread safe

不要。

使用多年前的现代 java.time classes 取代了麻烦的旧遗留日期时间 classes 例如 SimpleDateFormatjava.util.Datejava.sql.DateCalendar

java.time classes 设计为线程安全的。他们使用 immutable objects 模式,以 return 基于原始值的新对象,而不是“改变”(改变)原始对象。

使用智能对象,而不是哑字符串

我认为没有理由在您的示例代码中使用字符串:不在您的数据库访问代码中,不在您的业务对象中 (Trade)。

JDBC

从JDBC 4.2开始,我们可以与数据库交换java.time对象。对于类似于 SQL-标准 DATE 类型的数据库列,请使用 class LocalDateLocalDate class 表示没有时间和时区的仅日期值。

myPreparedStatement.setObject( … , myLocalDate ) ;

检索。

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

业务对象

您的 Trade class 应该将成员变量 startDate & endDate 作为 LocalDate 对象,而不是字符串。

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() …
    }
…
}

不需要字符串,不需要格式化模式。

字符串

要将日期时间值交换或存储为文本,请使用标准 ISO 8601 格式,而不是问题中所见的自定义格式。

java.time classes 在 parsing/generating 字符串时默认使用 ISO 8601 格式。因此无需指定格式模式。

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

为了在用户界面中呈现,让 java.time 自动本地化。要本地化,请指定:

  • FormatStyle 确定字符串的长度或缩写。
  • Locale判断:
    • 人类语言 翻译日名、月名等。
    • 文化规范决定缩写、大写、标点符号、分隔符等问题。

示例:

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

mardi 23 janvier 2018

DateTimeFormatter class 是线程安全的,设计为不可变对象。您可以持有一个跨线程使用的实例。


关于java.time

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.

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

要了解更多信息,请参阅 Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310

您可以直接与数据库交换 java.time 对象。使用 JDBC driver compliant with JDBC 4.2 或更高版本。不需要字符串,不需要 java.sql.* classes.

从哪里获得java.time classes?

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.

在这里您可以看到以线程安全方式使用日期格式的最快方法。因为您有 3 种方法可以做到:

  1. DateFormat.getDateInstance()
  2. synchronized
  3. 而远距离提供最佳性能的本地线程方式

完整代码示例:

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);
        }

    }

}