关于多线程:“Java DateFormat不是线程安全的”这会导致什么?

“Java DateFormat is not threadsafe” what does this leads to?

每个人都警告Java DateFormat不是线程安全的,理论上我理解这个概念。

但是我无法想象出由此产生的实际问题。 比如,我在类中有一个DateFormat字段,并且在多线程环境中的类(格式化日期)中的不同方法中使用相同的字段。

这会导致:

  • 格式异常等任何异常
  • 数据差异
  • 还有其他问题吗?

另外,请解释原因。


我们来试试吧。

这是一个程序,其中多个线程使用共享的SimpleDateFormat

程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void main(String[] args) throws Exception {

    final DateFormat format = new SimpleDateFormat("yyyyMMdd");

    Callable<Date> task = new Callable<Date>(){
        public Date call() throws Exception {
            return format.parse("20101022");
        }
    };

    //pool with 5 threads
    ExecutorService exec = Executors.newFixedThreadPool(5);
    List<Future<Date>> results = new ArrayList<Future<Date>>();

    //perform 10 date conversions
    for(int i = 0 ; i < 10 ; i++){
        results.add(exec.submit(task));
    }
    exec.shutdown();

    //look at the results
    for(Future<Date> result : results){
        System.out.println(result.get());
    }
}

运行几次,你会看到:

例外:

这里有一些例子:

1。

1
2
3
4
5
6
7
Caused by: java.lang.NumberFormatException: For input string:""
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48)
    at java.lang.Long.parseLong(Long.java:431)
    at java.lang.Long.parseLong(Long.java:468)
    at java.text.DigitList.getLong(DigitList.java:177)
    at java.text.DecimalFormat.parse(DecimalFormat.java:1298)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)

2。

1
2
3
4
5
6
Caused by: java.lang.NumberFormatException: For input string:".10201E.102014E4"
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1224)
    at java.lang.Double.parseDouble(Double.java:510)
    at java.text.DigitList.getDouble(DigitList.java:151)
    at java.text.DecimalFormat.parse(DecimalFormat.java:1303)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)

3。

1
2
3
4
5
6
7
Caused by: java.lang.NumberFormatException: multiple points
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1084)
    at java.lang.Double.parseDouble(Double.java:510)
    at java.text.DigitList.getDouble(DigitList.java:151)
    at java.text.DecimalFormat.parse(DecimalFormat.java:1303)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1936)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1312)

结果不正确:

1
2
3
4
5
6
7
8
9
10
Sat Oct 22 00:00:00 BST 2011
Thu Jan 22 00:00:00 GMT 1970
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Thu Oct 22 00:00:00 GMT 1970
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010

正确的结果:

1
2
3
4
5
6
7
8
9
10
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010
Fri Oct 22 00:00:00 BST 2010

在多线程环境中安全使用DateFormats的另一种方法是使用ThreadLocal变量来保存DateFormat对象,这意味着每个线程都有自己的副本,不需要等待其他线程释放它。这是如何:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class DateFormatTest {

  private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>(){
    @Override
    protected DateFormat initialValue() {
        return new SimpleDateFormat("yyyyMMdd");
    }
  };

  public Date convert(String source) throws ParseException{
    Date d = df.get().parse(source);
    return d;
  }
}

这是一篇包含更多细节的好文章。


我希望数据损坏 - 例如如果你同时解析两个日期,你可能会有一个被另一个日期的数据污染的电话。

很容易想象这是如何发生的:解析通常涉及到目前为止你所阅读的内容保持一定的状态。如果两个线程都在相同的状态下践踏,那么你会遇到问题。例如,DateFormat公开calendar类型的calendar字段,并查看SimpleDateFormat的代码,某些方法调用calendar.set(...),其他方法调用calendar.get(...)。这显然不是线程安全的。

我没有仔细研究DateFormat为什么不是线程安全的确切细节,但对我而言,如果没有同步就知道它是不安全的 - 非安全性的确切方式甚至可能在版本之间发生变化。

我个人会使用Joda Time的解析器,因为它们是线程安全的 - 而Joda Time是一个更好的日期和时间API开始:)


如果您使用的是Java 8,则可以使用DateTimeFormatter

A formatter created from a pattern can be used as many times as
necessary, it is immutable and is thread-safe.

码:

1
2
3
4
LocalDate date = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String text = date.format(formatter);
System.out.println(text);

输出:

1
2017-04-17

粗略地说,您不应将DateFormat定义为由许多线程访问的对象的实例变量,或static

Date formats are not synchronized. It is recommended to create separate format instances for each thread.

因此,如果您的Foo.handleBar(..)被多个线程访问,而不是:

1
2
3
4
5
6
7
public class Foo {
    private DateFormat df = new SimpleDateFormat("dd/mm/yyyy");

    public void handleBar(Bar bar) {
        bar.setFormattedDate(df.format(bar.getStringDate());  
    }
}

你应该使用:

1
2
3
4
5
6
7
public class Foo {

    public void handleBar(Bar bar) {
        DateFormat df = new SimpleDateFormat("dd/mm/yyyy");
        bar.setFormattedDate(df.format(bar.getStringDate());  
    }
}

此外,在所有情况下,都没有static DateFormat

如Jon Skeet所述,如果执行外部同步(即使用synchronized围绕对DateFormat的调用),您可以同时拥有静态变量和共享实例变量。


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.

这意味着假设你有一个DateFormat的对象,并且你正在从两个不同的线程访问同一个对象而你正在调用该对象的格式方法,两个线程将同时在同一个对象上输入同一个方法,这样你就可以想象它赢了结果不是很好

如果您必须使用DateFormat,那么您应该做些什么

1
2
3
public synchronized myFormat(){
// call here actual format method
}
  • 参考


在最好的答案中,dogbane给出了一个使用parse函数及其导致的示例。下面是一个代码,让你检查format功能。

请注意,如果更改执行程序(并发线程)的数量,您将得到不同的结果。从我的实验:

  • newFixedThreadPool设置为5,每次循环都会失败。
  • 设置为1并且循环将始终有效(显然所有任务实际上是逐个运行)
  • 设置为2,循环只有大约6%的工作机会。

我猜YMMV取决于你的处理器。

format函数通过格式化来自不同线程的时间而失败。这是因为内部format函数正在使用calendar对象,该对象是在format函数的开头设置的。 calendar对象是SimpleDateFormat类的属性。叹...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
 * Test SimpleDateFormat.format (non) thread-safety.
 *
 * @throws Exception
 */

private static void testFormatterSafety() throws Exception {
    final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    final Calendar calendar1 = new GregorianCalendar(2013,1,28,13,24,56);
    final Calendar calendar2 = new GregorianCalendar(2014,1,28,13,24,56);
    String expected[] = {"2013-02-28 13:24:56","2014-02-28 13:24:56"};

    Callable<String> task1 = new Callable<String>() {
        @Override
        public String call() throws Exception {
            return"0#" + format.format(calendar1.getTime());
        }
    };
    Callable<String> task2 = new Callable<String>() {
        @Override
        public String call() throws Exception {
            return"1#" + format.format(calendar2.getTime());
        }
    };

    //pool with X threads
    // note that using more then CPU-threads will not give you a performance boost
    ExecutorService exec = Executors.newFixedThreadPool(5);
    List<Future<String>> results = new ArrayList<>();

    //perform some date conversions
    for (int i = 0; i < 1000; i++) {
        results.add(exec.submit(task1));
        results.add(exec.submit(task2));
    }
    exec.shutdown();

    //look at the results
    for (Future<String> result : results) {
        String answer = result.get();
        String[] split = answer.split("#");
        Integer calendarNo = Integer.parseInt(split[0]);
        String formatted = split[1];
        if (!expected[calendarNo].equals(formatted)) {
            System.out.println("formatted:" + formatted);
            System.out.println("expected:" + expected[calendarNo]);
            System.out.println("answer:" + answer);
            throw new Exception("formatted != expected");
        /**
        } else {
            System.out.println("OK answer:" + answer);
        /**/

        }
    }
    System.out.println("OK: Loop finished");
}


Format,NumberFormat,DateFormat,MessageFormat等的规范并非设计为线程安全的。此外,parse方法调用Calendar.clone()方法并且它会影响日历占用空间,因此许多并发解析的线程将更改Calendar实例的克隆。

更多,这些是错误报告,如this和this,以及DateFormat线程安全问题的结果。


数据已损坏。昨天我注意到它在我的多线程程序中,我有静态DateFormat对象,并通过JDBC调用其值为format()。我有SQL select语句,我用不同的名称(SELECT date_from, date_from AS date_from1 ...)读取相同的日期。这些陈述在WHERE clasue中用于5个线程中的各种日期。日期看起来"正常",但它们的价值不同 - 而所有日期都是同一年,只有月份和日期发生变化。

其他答案向您展示了避免此类腐败的方法。我使DateFormat不是静态的,现在它是调用SQL语句的类的成员。我测试了同步静态版本。两者都运作良好,性能没有差异。


这是我的简单代码,显示DateFormat不是线程安全的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class DateTimeChecker {
    static DateFormat df = new SimpleDateFormat("EEE MMM dd kk:mm:ss z yyyy", Locale.ENGLISH);
    public static void main(String args[]){
       String target1 ="Thu Sep 28 20:29:30 JST 2000";
       String target2 ="Thu Sep 28 20:29:30 JST 2001";
       String target3 ="Thu Sep 28 20:29:30 JST 2002";
       runThread(target1);
       runThread(target2);
       runThread(target3);
   }
   public static void runThread(String target){
       Runnable myRunnable = new Runnable(){
          public void run(){

            Date result = null;
            try {
                result = df.parse(target);
            } catch (ParseException e) {
                e.printStackTrace();
                System.out.println("Ecxfrt");
            }  
            System.out.println(Thread.currentThread().getName() +" " + result);
         }
       };
       Thread thread = new Thread(myRunnable);

       thread.start();
     }
}

由于所有线程都使用相同的SimpleDateFormat对象,因此会抛出以下异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Exception in thread"Thread-0" Exception in thread"Thread-2" Exception in thread"Thread-1" java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(Unknown Source)
at sun.misc.FloatingDecimal.parseDouble(Unknown Source)
at java.lang.Double.parseDouble(Unknown Source)
at java.text.DigitList.getDouble(Unknown Source)
at java.text.DecimalFormat.parse(Unknown Source)
at java.text.SimpleDateFormat.subParse(Unknown Source)
at java.text.SimpleDateFormat.parse(Unknown Source)
at java.text.DateFormat.parse(Unknown Source)
at DateTimeChecker$1.run(DateTimeChecker.java:24)
at java.lang.Thread.run(Unknown Source)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(Unknown Source)
at sun.misc.FloatingDecimal.parseDouble(Unknown Source)
at java.lang.Double.parseDouble(Unknown Source)
at java.text.DigitList.getDouble(Unknown Source)
at java.text.DecimalFormat.parse(Unknown Source)
at java.text.SimpleDateFormat.subParse(Unknown Source)
at java.text.SimpleDateFormat.parse(Unknown Source)
at java.text.DateFormat.parse(Unknown Source)
at DateTimeChecker$1.run(DateTimeChecker.java:24)
at java.lang.Thread.run(Unknown Source)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(Unknown Source)
at sun.misc.FloatingDecimal.parseDouble(Unknown Source)
at java.lang.Double.parseDouble(Unknown Source)
at java.text.DigitList.getDouble(Unknown Source)
at java.text.DecimalFormat.parse(Unknown Source)
at java.text.SimpleDateFormat.subParse(Unknown Source)
at java.text.SimpleDateFormat.parse(Unknown Source)
at java.text.DateFormat.parse(Unknown Source)
at DateTimeChecker$1.run(DateTimeChecker.java:24)
at java.lang.Thread.run(Unknown Source)

但是如果我们将不同的对象传递给不同的线程,代码就会运行
没有错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class DateTimeChecker {
    static DateFormat df;
    public static void main(String args[]){
       String target1 ="Thu Sep 28 20:29:30 JST 2000";
       String target2 ="Thu Sep 28 20:29:30 JST 2001";
       String target3 ="Thu Sep 28 20:29:30 JST 2002";
       df = new SimpleDateFormat("EEE MMM dd kk:mm:ss z yyyy", Locale.ENGLISH);
       runThread(target1, df);
       df = new SimpleDateFormat("EEE MMM dd kk:mm:ss z yyyy", Locale.ENGLISH);
       runThread(target2, df);
       df = new SimpleDateFormat("EEE MMM dd kk:mm:ss z yyyy", Locale.ENGLISH);
       runThread(target3, df);
   }
   public static void runThread(String target, DateFormat df){
      Runnable myRunnable = new Runnable(){
        public void run(){

            Date result = null;
            try {
                result = df.parse(target);
            } catch (ParseException e) {
                e.printStackTrace();
                System.out.println("Ecxfrt");
            }  
            System.out.println(Thread.currentThread().getName() +" " + result);
         }
       };
       Thread thread = new Thread(myRunnable);

       thread.start();
   }
}

这些是结果。

1
2
3
Thread-0  Thu Sep 28 17:29:30 IST 2000
Thread-2  Sat Sep 28 17:29:30 IST 2002
Thread-1  Fri Sep 28 17:29:30 IST 2001


如果有多个线程操纵/访问单个DateFormat实例并且未使用同步,则可能会得到加扰结果。那是因为多个非原子操作可能会改变状态或看到内存不一致。