Java的SimpleDateFormat不是线程安全的,多线程环境下慎用

x33g5p2x  于2021-10-16 转载在 Java  
字(7.1k)|赞(0)|评价(0)|浏览(370)

SimpleDateFormat 用于在 Java 中格式化和解析日期。

您可以使用 yyyy-MM-dd HH:mm:ss 之类的日期时间模式创建 SimpleDateFormat 的实例,然后使用该实例将日期格式化并解析为字符串。

关于 SimpleDateFormat 类需要注意的最重要的事情之一是它不是线程安全的,如果使用不当会导致多线程环境中的问题。

我写这篇文章是因为我看到开发人员在多线程环境中盲目使用 SimpleDateFormat 而不知道和处理它不是线程安全的事实。

SimpleDateFormat 线程安全问题示例

让我们了解当我们尝试在没有任何同步的多线程环境中使用 SimpleDateFormat 时会发生什么。

下面是一个非常简单的类的例子,我们用预定义的模式解析给定的日期,但我们从多个线程并发

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

public class SimpleDateFormatThreadUnsafetyExample {

    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");

    public static void main(String[] args) {
        String dateStr = "2018-06-22T10:00:00";

        ExecutorService executorService = Executors.newFixedThreadPool(10);
        Runnable task = new Runnable() {
            @Override
            public void run() {
                parseDate(dateStr);
            }
        };

        for(int i = 0; i < 100; i++) {
            executorService.submit(task);
        }

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

够简单!我们有一个 SimpleDateFormat 实例,用于解析来自多个线程的日期。

如果你运行上面的程序,它会产生这样的输出(你的输出可能与我的不同,但会有类似的错误)-

# Output
Successfully Parsed Date Fri Jun 22 10:00:00 IST 2018
Successfully Parsed Date Thu Jun 22 10:00:00 IST 2220
java.lang.NumberFormatException: multiple points
        at java.base/jdk.internal.math.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
        at java.base/jdk.internal.math.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
        at java.base/java.lang.Double.parseDouble(Double.java:543)
        at java.base/java.text.DigitList.getDouble(DigitList.java:169)
        at java.base/java.text.DecimalFormat.parse(DecimalFormat.java:2098)
        at java.base/java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1915)
        at java.base/java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1529)
        at java.base/java.text.DateFormat.parse(DateFormat.java:386)
        at SimpleDateFormatThreadUnsafetyExample.parseDate(SimpleDateFormatThreadUnsafetyExample.java:32)
        at SimpleDateFormatThreadUnsafetyExample.access$000(SimpleDateFormatThreadUnsafetyExample.java:8)
        at SimpleDateFormatThreadUnsafetyExample$1.run(SimpleDateFormatThreadUnsafetyExample.java:19)
        at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:514)
        at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.base/java.lang.Thread.run(Thread.java:844)
java.lang.NumberFormatException: For input string: ""
        at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
        at java.base/java.lang.Long.parseLong(Long.java:702)
        at java.base/java.lang.Long.parseLong(Long.java:817)
        at java.base/java.text.DigitList.getLong(DigitList.java:195)
        at java.base/java.text.DecimalFormat.parse(DecimalFormat.java:2093)
        at java.base/java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2222)
        at java.base/java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1529)
        at java.base/java.text.DateFormat.parse(DateFormat.java:386)
        at SimpleDateFormatThreadUnsafetyExample.parseDate(SimpleDateFormatThreadUnsafetyExample.java:32)
        at SimpleDateFormatThreadUnsafetyExample.access$000(SimpleDateFormatThreadUnsafetyExample.java:8)
        at SimpleDateFormatThreadUnsafetyExample$1.run(SimpleDateFormatThreadUnsafetyExample.java:19)
        at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:514)
        at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.base/java.lang.Thread.run(Thread.java:844)
Successfully Parsed Date Fri Jun 22 10:00:00 IST 2018
Successfully Parsed Date Fri Jun 22 10:00:00 IST 2018
Successfully Parsed Date Fri Jun 22 10:00:00 IST 2018
Successfully Parsed Date Fri Jun 22 10:00:00 IST 2018
Successfully Parsed Date Fri Jun 22 10:00:00 IST 2018

## More output....... (Omitted for brevity)

SimpleDateFormat 类会改变其内部状态以格式化和解析日期。这就是为什么当多个线程同时使用 SimpleDateFormat 的同一个实例时会导致这些问题。

如何在多线程环境中使用 SimpleDateFormat?

你有两个选择——

为每个线程创建一个新的 SimpleDateFormat 实例。
*
使用 synchronized 关键字或 lock 同步多个线程的并发访问。

1. 为每个线程创建 SimpleDateFormat 的单独实例
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SimpleDateFormatThreadUnsafetyExample {

    public static void main(String[] args) {
        String dateStr = "2018-06-22T10:00:00";

        ExecutorService executorService = Executors.newFixedThreadPool(10);
        Runnable task = new Runnable() {
            @Override
            public void run() {
                parseDate(dateStr);
            }
        };

        for(int i = 0; i < 100; i++) {
            executorService.submit(task);
        }

        executorService.shutdown();
    }

    private static void parseDate(String dateStr) {
        try {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
            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();
        }
    }
}

请注意,我已将 SimpleDateFormat 实例创建移到 parseDate() 方法中。这样,我们为每个线程创建了一个新实例。上面程序的输出不会有任何错误 -

# Output
Successfully Parsed Date Fri Jun 22 10:00:00 IST 2018
Successfully Parsed Date Fri Jun 22 10:00:00 IST 2018
Successfully Parsed Date Fri Jun 22 10:00:00 IST 2018
Successfully Parsed Date Fri Jun 22 10:00:00 IST 2018
Successfully Parsed Date Fri Jun 22 10:00:00 IST 2018
Successfully Parsed Date Fri Jun 22 10:00:00 IST 2018
Successfully Parsed Date Fri Jun 22 10:00:00 IST 2018
Successfully Parsed Date Fri Jun 22 10:00:00 IST 2018
Successfully Parsed Date Fri Jun 22 10:00:00 IST 2018

## More output....... (Omitted for brevity)
2. 共享 SimpleDateFormat 的同一个实例但同步并发访问
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SimpleDateFormatThreadUnsafetyExample {

    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");

    public static void main(String[] args) {
        String dateStr = "2018-06-22T10:00:00";

        ExecutorService executorService = Executors.newFixedThreadPool(10);
        Runnable task = new Runnable() {
            @Override
            public void run() {
                parseDate(dateStr);
            }
        };

        for(int i = 0; i < 100; i++) {
            executorService.submit(task);
        }

        executorService.shutdown();
    }

    private synchronized 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();
        }
    }
}

在上面的例子中,我在 parseDate() 方法中添加了一个 synchronized 关键字。在这种情况下,一次只有一个线程可以进入 parseDate() 方法。

我的建议 - 不要使用 SimpleDateFormat。

是的!不要使用 SimpleDateFormat。 Java 8 有一个更好更增强的 DateTimeFormatter,它也是线程安全的。

您还应该避免使用 DateCalendar 类,并尝试使用 Java 8 DateTime 类,如 OffsetDateTimeZonedDateTimeLocalDateTimeLocalDateLocalTime 等。它们比 DateCalendar 类具有更多的功能。

直到下一次…

相关文章