SimpleDateFormat非线程安全及解决方法

【RAEDME】

SimpleDateFormat非线程安全,即在多线程环境下解析字符串为日期对象,或格式化日期为字符串时,会抛出异常;当然,这是一个老生常谈的问题;

本文参考了已有 SimpleDateFormat的分析文章,做了总结,包括SimpleDateFormat报错代码,解决方法代码。

解决方法包括:

  • 方法1:SimpleDateFormat 使用单例;
  • 方法2:为解析方法加锁;避免多线程环境;
  • 方法3: 使用 ThreadLocal; 每个线程获取的都是一个SimpleDateFormat单例,与方法1类似;
  • 方法4: 使用java8提供的 DateTimeFormatter (推荐);

参考资料:

Java's SimpleDateFormat is not thread-safe, Use carefully in multi-threaded environments | CalliCoderThe SimpleDateFormat class in Java is not thread-safe. You should either create separate instances of SimpleDateFormat for every thread, or synchronize concurrent access by multiple threads with a synchronized keyword or a lock.icon-default.png?t=N7T8https://www.callicoder.com/java-simpledateformat-thread-safety-issues/


【1】SimpleDateFormat在多线程环境下解析字符串报错

/**
 * @Description simple data format 解析失败
 * @author xiao tang
 * @version 1.0.0
 * @createTime 2023年10月05日
 */
public class SimpleDateFormatError {
    private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd");

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable task = () -> parse("20231005");
        // 线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        ArrayList> results = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            results.add(threadPool.submit(task));
        }
        // 获取异步执行结果
        for (Future result : results) {
            System.out.println(result.get());
        }
        threadPool.shutdown();
    }

    public static Date parse(String dateStr) {
        try {
            return SIMPLE_DATE_FORMAT.parse(dateStr);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

结果:

java.lang.NumberFormatException: multiple points
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
	at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
	at java.lang.Double.parseDouble(Double.java:538)
	at java.text.DigitList.getDouble(DigitList.java:169)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1867)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at com.java8.datetime.SimpleDateFormatError.parse(SimpleDateFormatError.java:34)
	at com.java8.datetime.SimpleDateFormatError.lambda$main$0(SimpleDateFormatError.java:18)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: multiple points
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
	at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
	at java.lang.Double.parseDouble(Double.java:538)
	at java.text.DigitList.getDouble(DigitList.java:169)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1867)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at com.java8.datetime.SimpleDateFormatError.parse(SimpleDateFormatError.java:34)
	at com.java8.datetime.SimpleDateFormatError.lambda$main$0(SimpleDateFormatError.java:18)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: multiple points
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
	at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
	at java.lang.Double.parseDouble(Double.java:538)
	at java.text.DigitList.getDouble(DigitList.java:169)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1867)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at com.java8.datetime.SimpleDateFormatError.parse(SimpleDateFormatError.java:34)
	at com.java8.datetime.SimpleDateFormatError.lambda$main$0(SimpleDateFormatError.java:18)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: multiple points
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
	at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
	at java.lang.Double.parseDouble(Double.java:538)
	at java.text.DigitList.getDouble(DigitList.java:169)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1867)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at com.java8.datetime.SimpleDateFormatError.parse(SimpleDateFormatError.java:34)
	at com.java8.datetime.SimpleDateFormatError.lambda$main$0(SimpleDateFormatError.java:18)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: multiple points
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
	at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
	at java.lang.Double.parseDouble(Double.java:538)
	at java.text.DigitList.getDouble(DigitList.java:169)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1867)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at com.java8.datetime.SimpleDateFormatError.parse(SimpleDateFormatError.java:34)
	at com.java8.datetime.SimpleDateFormatError.lambda$main$0(SimpleDateFormatError.java:18)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: multiple points
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
	at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
	at java.lang.Double.parseDouble(Double.java:538)
	at java.text.DigitList.getDouble(DigitList.java:169)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1867)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at com.java8.datetime.SimpleDateFormatError.parse(SimpleDateFormatError.java:34)
	at com.java8.datetime.SimpleDateFormatError.lambda$main$0(SimpleDateFormatError.java:18)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: multiple points
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
	at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
	at java.lang.Double.parseDouble(Double.java:538)
	at java.text.DigitList.getDouble(DigitList.java:169)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1867)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at com.java8.datetime.SimpleDateFormatError.parse(SimpleDateFormatError.java:34)
	at com.java8.datetime.SimpleDateFormatError.lambda$main$0(SimpleDateFormatError.java:18)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: multiple points
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
	at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
	at java.lang.Double.parseDouble(Double.java:538)
	at java.text.DigitList.getDouble(DigitList.java:169)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1867)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at com.java8.datetime.SimpleDateFormatError.parse(SimpleDateFormatError.java:34)
	at com.java8.datetime.SimpleDateFormatError.lambda$main$0(SimpleDateFormatError.java:18)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
null
null
null
null
null
null
null
null
Thu Oct 05 00:00:00 CST 2023
Thu Oct 05 00:00:00 CST 2023

分析,显然,在多线程环境下,SimpleDateFormat解析字符串到日期对象部分失败;

原因:SimpleDateFormat在解析时,会改变其内部状态以格式化或解析日期。多线程环境下,线程1用的内部状态是线程2修改后的,存在状态不一致的问题;


【2】解决方法

本文给出的解决方法有4种:

  • 方法1:使用 SimpleDateFormat 单例执行解析或格式化操作;
  • 方法2:对调用SimpleDateFormat api的业务方法加锁;如添加 Synchronized关键字,加Lock;
  • 方法3:使用 ThreadLocal; 每个线程获取的都是一个SimpleDateFormat单例;
  • 方法4: 使用java8提供的 DateTimeFormatter ;

【2.1】使用 SimpleDateFormat 单例(不推荐)

/**
 * @Description simple data format 解析失败的解决方法1-SimpleDateFormat使用单例(每次调用都新建一个实例)
 * @author xiao tang
 * @version 1.0.0
 * @createTime 2023年10月05日
 */
public class SimpleDateFormatErrorSolutionWithInstance {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable task = () -> parse("20231005");
        // 线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        ArrayList> results = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            results.add(threadPool.submit(task));
        }
        // 获取异步执行结果
        for (Future result : results) {
            System.out.println(result.get());
        }
        threadPool.shutdown();
    }

    public static Date parse(String dateStr) {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
        try {
            return simpleDateFormat.parse(dateStr);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null; 
    }
}

结果:

Thu Oct 05 00:00:00 CST 2023
Thu Oct 05 00:00:00 CST 2023
Thu Oct 05 00:00:00 CST 2023
Thu Oct 05 00:00:00 CST 2023
Thu Oct 05 00:00:00 CST 2023
Thu Oct 05 00:00:00 CST 2023
Thu Oct 05 00:00:00 CST 2023
Thu Oct 05 00:00:00 CST 2023
Thu Oct 05 00:00:00 CST 2023
Thu Oct 05 00:00:00 CST 2023

Process finished with exit code 0

【2.2】对业务方法加锁(不推荐)

本文使用了 synchronized,parse方法会同步执行;没有多线程环境;

/**
 * @Description simple data format 解析失败的解决方法2-加锁
 * @author xiao tang
 * @version 1.0.0
 * @createTime 2023年10月05日
 */
public class SimpleDateFormatErrorSolutionWithLock {

    private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd");

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable task = () -> parse("20231005");
        // 线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        ArrayList> results = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            results.add(threadPool.submit(task));
        }
        // 获取异步执行结果
        for (Future result : results) {
            System.out.println(result.get());
        }
        threadPool.shutdown();
    }

    public synchronized static Date parse(String dateStr) {
        try {
            return SIMPLE_DATE_FORMAT.parse(dateStr);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

【2.3】使用 ThreadLocal特别不推荐,仅了解)

因为ThreadLocal创建的资源属于线程级别,处理不好,容易造成内存泄漏;

/**
 * @Description simple data format 解析失败的解决方法3-使用 ThreadLocal
 * @author xiao tang
 * @version 1.0.0
 * @createTime 2023年10月05日
 */
public class SimpleDateFormatErrorSolutionWithThreadLocal {

    private static final ThreadLocal THREAD_LOCAL = new ThreadLocal() {
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyyMMdd");
        }
    };

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable task = () -> {
            Date result = parse("20231005");
            // 这里要清除当前线程持有的线程级变量-SimpleDateFormat单例,否则会造成内存泄漏
            THREAD_LOCAL.remove();
            return result; 
        };
        // 线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        ArrayList> results = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            results.add(threadPool.submit(task));
        }
        // 获取异步执行结果
        for (Future result : results) {
            System.out.println(result.get());
        }
        threadPool.shutdown();
    }

    public synchronized static Date parse(String dateStr) {
        try {
            return THREAD_LOCAL.get().parse(dateStr);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

【2.4】使用java8的DateTimeFormatter  (推荐)

/**
 * @Description simple data format 解析失败的解决方法3-使用 java8 DateTimeFormatter
 * @author xiao tang
 * @version 1.0.0
 * @createTime 2023年10月05日
 */
public class SimpleDateFormatErrorSolutionWithJava8DateTimeFormatter {

    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable task = () -> parse("20231005");
        // 线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        ArrayList> results = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            results.add(threadPool.submit(task));
        }
        // 获取异步执行结果
        for (Future result : results) {
            System.out.println(result.get());
        }
        threadPool.shutdown();
    }

    public synchronized static LocalDate parse(String dateStr) {
        try {
            return LocalDate.parse(dateStr, DATE_TIME_FORMATTER);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

【3】日期格式化与解析建议

不要使用 SimpleDateFormat;java8提供了更好的和增强的 DateTimeFormatter,且是线程安全的;

也应该避免使用 Date, Calendar 类; 尝试使用 java8 DateTime 类,如 OffsetDateTime, ZoneDateTime, LocalDateTime, LocalDate, LocalTime 等;java8提供的日期时间类比 Date, Calendar 有更好的性能;

你可能感兴趣的:(java8,java)