使SimpleDateFormat线程安全

时间:2018-07-16 16:25:41

标签: java multithreading thread-safety simpledateformat

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

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

这是我的代码:

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直接替换吗?

是否存在使该线程安全的更好的替代方法?

3 个答案:

答案 0 :(得分:3)

tl; dr

使用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 )
)
  

2018年1月23日

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

java.time

  

使SimpleDateFormat线程安全

不要。

使用多年前的现代 java.time 类取代了麻烦的旧的传统日期时间类,例如SimpleDateFormatjava.util.Datejava.sql.DateCalendar

java.time 类被设计为线程安全的。他们使用immutable objects模式,根据原始值返回新鲜的对象,而不是“变异”(更改)原始值。

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

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

JDBC

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

myPreparedStatement.setObject( … , myLocalDate ) ;

检索。

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

业务对象

您的Trade类应将成员变量startDateendDate作为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 类在解析/生成字符串时默认使用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 2018年1月23日

根据设计,DateTimeFormatter类是线程安全的,作为不可变的对象。您可以持有一个要在线程间使用的实例。


关于 java.time

java.time框架已内置在Java 8及更高版本中。这些类取代了麻烦的旧legacy日期时间类,例如java.util.DateCalendarSimpleDateFormat

目前位于Joda-Timemaintenance mode项目建议迁移到java.time类。

要了解更多信息,请参见Oracle Tutorial。并在Stack Overflow中搜索许多示例和说明。规格为JSR 310

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

在哪里获取java.time类?

ThreeTen-Extra项目使用其他类扩展了java.time。该项目为将来可能在java.time中添加内容提供了一个试验场。您可能会在这里找到一些有用的类,例如IntervalYearWeekYearQuartermore

答案 1 :(得分:2)

您可以将其设为ThreadLocal。池中的每个线程都将拥有自己的格式化程序。

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

答案 2 :(得分:0)

在这里,您可以看到以线程安全的方式使用日期格式的最快方法。因为您有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);
        }

    }

}