SimpleDateFormat 线程安全问题修复方案

这篇具有很好参考价值的文章主要介绍了SimpleDateFormat 线程安全问题修复方案。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

问题介绍

在日常的开发过程中,我们不可避免地会使用到 JDK8 之前的 Date 类,在格式化日期或解析日期时就需要用到 SimpleDateFormat 类,但由于该类并不是线程安全的,所以我们常发现对该类的不恰当使用会导致日期解析异常,从而影响线上服务可用率。

以下是对 SimpleDateFormat 类不恰当使用的示例代码:

package com.jd.threadsafe;

import java.text.SimpleDateFormat;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @Date: 2023/7/25 10:47
 * @Desc: SimpleDateFormat 线程安全问题复现
 * @Version: V1.0
 **/
public class SimpleDateFormatTest {
    private static final AtomicBoolean STOP = new AtomicBoolean();
    private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("yyyy-M-d"); // 非线程安全

    public static void main(String[] args) {
        Runnable runnable = () -> {
            int count = 0;
            while (!STOP.get()) {
                try {
                    FORMATTER.parse("2023-7-15");
                } catch (Exception e) {
                    e.printStackTrace();
                    if (++count > 3) {
                        STOP.set(true);
                    }
                }
            }
        };

        new Thread(runnable).start();
        new Thread(runnable).start();
    }
}



以上代码模拟了多线程并发使用 SimpleDateFormat 实例的场景,此时可观察到如下异常输出:

java.lang.NumberFormatException: For input string: ""
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Long.parseLong(Long.java:601)
	at java.lang.Long.parseLong(Long.java:631)
	at java.text.DigitList.getLong(DigitList.java:195)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2082)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at com.jd.threadsafe.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:21)
	at java.lang.Thread.run(Thread.java:750)
java.lang.NumberFormatException: For input string: ""
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Long.parseLong(Long.java:601)
	at java.lang.Long.parseLong(Long.java:631)
	at java.text.DigitList.getLong(DigitList.java:195)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2082)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at com.jd.threadsafe.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:21)
	at java.lang.Thread.run(Thread.java:750)
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:2087)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at com.jd.threadsafe.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:21)
	at java.lang.Thread.run(Thread.java:750)
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:2087)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at com.jd.threadsafe.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:21)
	at java.lang.Thread.run(Thread.java:750)
java.lang.NumberFormatException: For input string: ""
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Long.parseLong(Long.java:601)
	at java.lang.Long.parseLong(Long.java:631)
	at java.text.DigitList.getLong(DigitList.java:195)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2082)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at com.jd.threadsafe.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:21)
	at java.lang.Thread.run(Thread.java:750)



以上异常的根本原因是因为 SimpleDateFormat 是有状态的,如 SimpleDateFormat 类中含有非线程安全的 NumberFormat 成员变量:

/**
 * The number formatter that <code>DateFormat</code> uses to format numbers
 * in dates and times.  Subclasses should initialize this to a number format
 * appropriate for the locale associated with this <code>DateFormat</code>.
 * @serial
 */
protected NumberFormat numberFormat;



从 NumberFormat 的 Java Doc 中能看到如下描述:

Synchronization Number formats are generally 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.

从 SimpleDateFormat 的 Java Doc 中能看到如下描述:

Synchronization 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.

修复方案一:加锁(不推荐)

package com.jd.threadsafe;

import java.text.SimpleDateFormat;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @Date: 2023/7/25 10:47
 * @Desc: SimpleDateFormat 线程安全修复方案:加锁
 * @Version: V1.0
 **/
public class SimpleDateFormatLockTest {
    private static final AtomicBoolean STOP = new AtomicBoolean();
    private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("yyyy-M-d"); // 非线程安全

    public static void main(String[] args) {
        Runnable runnable = () -> {
            int count = 0;
            while (!STOP.get()) {
                try {
                    synchronized (FORMATTER) {
                        FORMATTER.parse("2023-7-15");
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    if (++count > 3) {
                        STOP.set(true);
                    }
                }
            }
        };

        new Thread(runnable).start();
        new Thread(runnable).start();
    }

}



首先我们能想到的最简单的解决线程安全问题的修复方案即加锁,如以上修复方案,使用 synchronized 关键字对 FORMATTER 实例进行加锁,此时多线程进行日期格式化时退化为串行执行,保证了正确性牺牲了性能,不推荐。

修复方案二:栈封闭(不推荐)

如果按照文档中的推荐用法,可知推荐为每个线程创建独立的 SimpleDateFormat 实例,一种最简单的方式就是在方法调用时每次创建 SimpleDateFormat 实例,以实现栈封闭的效果,如以下示例代码:

package com.jd.threadsafe;

import java.text.SimpleDateFormat;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @Date: 2023/7/25 10:47
 * @Desc: SimpleDateFormat 线程安全修复方案:栈封闭
 * @Version: V1.0
 **/
public class SimpleDateFormatStackConfinementTest {
    private static final AtomicBoolean STOP = new AtomicBoolean();

    public static void main(String[] args) {
        Runnable runnable = () -> {
            int count = 0;
            while (!STOP.get()) {
                try {
                    new SimpleDateFormat("yyyy-M-d").parse("2023-7-15");
                } catch (Exception e) {
                    e.printStackTrace();
                    if (++count > 3) {
                        STOP.set(true);
                    }
                }
            }
        };

        new Thread(runnable).start();
        new Thread(runnable).start();
    }

}



即将共用的 SimpleDateFormat 实例调整为每次创建新的实例,该修复方案保证了正确性但每次方法调用需要创建 SimpleDateFormat 实例,并未复用 SimpleDateFormat 实例,存在 GC 损耗,所以并不推荐。

修复方案三:ThreadLocal(推荐)

如果日期格式化操作是应用里的高频操作,且需要优先保证性能,那么建议每个线程复用 SimpleDateFormat 实例,此时可引入 ThreadLocal 类来解决该问题:

package com.jd.threadsafe;

import java.text.SimpleDateFormat;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @Date: 2023/7/25 10:47
 * @Desc: SimpleDateFormat 线程安全修复方案:ThreadLocal
 * @Version: V1.0
 **/
public class SimpleDateFormatThreadLocalTest {
    private static final AtomicBoolean STOP = new AtomicBoolean();
    private static final ThreadLocal<SimpleDateFormat> SIMPLE_DATE_FORMAT_THREAD_LOCAL = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-M-d"));

    public static void main(String[] args) {
        Runnable runnable = () -> {
            int count = 0;
            while (!STOP.get()) {
                try {
                    SIMPLE_DATE_FORMAT_THREAD_LOCAL.get().parse("2023-7-15");
                } catch (Exception e) {
                    e.printStackTrace();
                    if (++count > 3) {
                        STOP.set(true);
                    }
                }
            }
        };

        new Thread(runnable).start();
        new Thread(runnable).start();
    }

}



执行上述代码,不会再观察到异常输出,因为已为每个线程创建了独立的 SimpleDateFormat 实例,即在线程维度复用了 SimpleDateFormat 实例,在线程池等池化场景下相比上方栈封闭的修复方案降低了 GC 损耗,同时也规避了线程安全问题。

以上使用 ThreadLocal 在线程维度复用非线程安全的实例可认为是一种通用的模式,可在 JDK 及不少开源项目中看到类似的模式实现,如在 JDK 最常见的 String 类中,对字符串进行编解码所需要用到的 StringDecoder 及 StringEncoder 即使用了 ThreadLocal 来规避线程安全问题:

/**
 * Utility class for string encoding and decoding.
 */
class StringCoding {

    private StringCoding() { }

    /** The cached coders for each thread */
    private final static ThreadLocal<SoftReference<StringDecoder>> decoder =
        new ThreadLocal<>();
    private final static ThreadLocal<SoftReference<StringEncoder>> encoder =
        new ThreadLocal<>();

    // ...
}



参考:JDK8 - StringCoding

在 Dubbo 的 ThreadLocalKryoFactory 类中,在对非线程安全类 Kryo 的使用中,也使用了 ThreadLocal 类来规避线程安全问题:

package org.apache.dubbo.common.serialize.kryo.utils;

import com.esotericsoftware.kryo.Kryo;

public class ThreadLocalKryoFactory extends AbstractKryoFactory {

    private final ThreadLocal<Kryo> holder = new ThreadLocal<Kryo>() {
        @Override
        protected Kryo initialValue() {
            return create();
        }
    };

    @Override
    public void returnKryo(Kryo kryo) {
        // do nothing
    }

    @Override
    public Kryo getKryo() {
        return holder.get();
    }
}



参考:Dubbo - ThreadLocalKryoFactory

类似地,在 HikariCP 的 ConcurrentBag 类中,也用到了 ThreadLocal 类来规避线程安全问题,此处不再进一步展开。

修复方案四:FastDateFormat(推荐)

针对 SimpleDateFormat 类的线程安全问题,apache commons-lang 提供了 FastDateFormat 类。其部分 Java Doc 如下:

FastDateFormat is a fast and thread-safe version ofSimpleDateFormat. To obtain an instance of FastDateFormat, use one of the static factory methods:getInstance(String, TimeZone, Locale),getDateInstance(int, TimeZone, Locale),getTimeInstance(int, TimeZone, Locale), orgetDateTimeInstance(int, int, TimeZone, Locale) Since FastDateFormat is thread safe, you can use a static member instance: private static final FastDateFormat DATE_FORMATTER = FastDateFormat.getDateTimeInstance(FastDateFormat.LONG, FastDateFormat.SHORT); This class can be used as a direct replacement toSimpleDateFormatin most formatting and parsing situations. This class is especially useful in multi-threaded server environments.SimpleDateFormatis not thread-safe in any JDK version, nor will it be as Sun have closed the bug/RFE. All patterns are compatible with SimpleDateFormat (except time zones and some year patterns - see below).

该修复方案相对来说代码改造最小,仅需在声明静态 SimpleDateFormat 实例代码处将 SimpleDateFormat 实例替换为 FastDateFormat 实例,示例代码如下:

package com.jd.threadsafe;

import org.apache.commons.lang3.time.FastDateFormat;

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @Date: 2023/7/6 20:05
 * @Desc: SimpleDateFormat 线程安全修复方案:FastDateFormat
 * @Version: V1.0
 **/
public class FastDateFormatTest {
    private static final AtomicBoolean STOP = new AtomicBoolean();
    private static final FastDateFormat FORMATTER = FastDateFormat.getInstance("yyyy-M-d");

    public static void main(String[] args) {
        Runnable runnable = () -> {
            int count = 0;
            while (!STOP.get()) {
                try {
                    FORMATTER.parse("2023-7-15");
                } catch (Exception e) {
                    e.printStackTrace();
                    if (++count > 3) {
                        STOP.set(true);
                    }
                }
            }
        };

        new Thread(runnable).start();
        new Thread(runnable).start();
    }

}



执行上述代码,不会再观察到异常输出,因为 FastDateFormat 是线程安全的实现,支持多线程并发调用。

总结

无论使用哪种修复方案,都需要在修改后进行充分的测试,保证修复后不影响原有业务逻辑,如通过单元测试、流量回放等方式来保证本次修复的正确性。

思考

代码里使用 SimpleDateFormat 类的原因是因为日期使用了 Date 类,与 Date 相配套的 JDK 格式化类即 SimpleDateFormat 类,如果我们在处理日期时使用 JDK8 引入的 LocalDateTime 等不可变日期类,那么格式化将使用配套的线程安全的 DateTimeFormatter 类,从根源上规避掉对非线程安全类 SimpleDateFormat 类的使用。

作者:京东物流 刘建设 张九龙 田爽

来源:京东云开发者社区 自猿其说Tech 转载请注明来源文章来源地址https://www.toymoban.com/news/detail-654865.html

到了这里,关于SimpleDateFormat 线程安全问题修复方案的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【多线程】线程安全问题原因与解决方案

    目录 线程安全的概念 线程不安全示例 线程不安全的原因      多个线程修改了同一个变量     线程是抢占式执行的     原子性     内存可见性     有序性 线程不安全解决办法  synchronized -监视器锁monitor lock     synchronized 的特性         互斥         刷新内

    2024年02月06日
    浏览(25)
  • java 线程安全问题 三种线程同步方案 线程通信(了解)

    线程安全问题指的是,多个线程同时操作同一个共享资源的时候,可能会出现业务安全问题。 下面代码演示上述问题,先定义一个共享的账户类: 在定义一个取钱的线程类 最后,再写一个测试类,在测试类中创建两个线程对象 某个执行结果: 为了解决前面的线程安全问题,

    2024年02月09日
    浏览(32)
  • 【线程安全】死锁问题及解决方案

    比如上一次讲到 synchronized 的时候,一个线程,对同一个对象连续加锁两次,如果出现阻塞等待,代表这个锁是不可重入锁,这样的线程,也就称为死锁! 一旦程序进入死锁了就会导致线程僵住了,无法继续执行后续的工作了,程序也就出现了严重的 BUG! 而死锁这样的情况

    2024年02月06日
    浏览(35)
  • 线程安全问题的原因和解决方案

    如果某个代码,在单线程执行下没有问题,在多线程执行下执行也没有问题,则称“线程安全”,反之称“线程不安全”。 目录 前言 一、简述线程不安全案例 二、线程安全问题的原因 (一)(根本问题)线程调度是随机的 (二)代码的结构问题 (三)代码执行不是原子的

    2024年02月14日
    浏览(29)
  • 【JavaEE面试题(九)线程安全问题的原因和解决方案】

    大家观察下是否适用多线程的现象是否一致?同时尝试思考下为什么会有这样的现象发生呢? 原因是 1.load 2. add 3. save 注意:可能会导致 小于5w 想给出一个线程安全的确切定义是复杂的,但我们可以这样认为: 如果多线程环境下代码运行的结果是符合我们预期的,即在单线

    2024年02月16日
    浏览(34)
  • Curl多线程https访问,崩溃问题修复

    Curl: ��: SSL and multithread crash on windows, how to use mutex on windows? SSL and multithread crash on windows, how to use mutex on windows? From: mao mao lmjrd_at_hotmail.com Date: Fri, 25 Nov 2016 09:50:48 +0000 Thankyou Thomas and Johan both to Johan: how to use these two functions openssl_crypto_locking_callback openssl_crypto_init_locks when star

    2024年02月03日
    浏览(30)
  • iOS开发进阶(六):Xcode14 使用信号量造成线程优先级反转问题修复

    应用 Xcode 14.1 进行项目编译时,遇到以下错误提示,导致APP线程暂停。 以上问题是由于iOS信号量造成线程优先级反转,在并发队列使用信号量会可能会造成线程优先级反转。 经过查询资料,发现是在 XCode14 上增加了工具,比如 : Thread Performance Checker ( XCode14 上默认开启的)

    2024年02月01日
    浏览(49)
  • Java SimpleDateFormat设置时区导致时间不正确的解决方案

    问题介绍 在Android开发中经常遇到有的接口需要上传当前时间,如果后台要求直接传一个long类型的时间戳还好,因为这个时间戳是跟时区无关的,如果后台接口要求传的是格式化的时间,若本地设备设置的时区与后台要就的时区不一致,就会导致上传的时间不准确。 不完善的

    2024年02月11日
    浏览(32)
  • macOS下 /etc/hosts 文件权限问题修复方案

    macOS下 etc/hosts 文件权限问题修复 前言 当在 macOS 上使用 vi 编辑 /etc/hosts 文件时发现出现 Permission Denied 的提示,就算在前面加上 sudo 也照样出现一样的提示,解决方案如下; 解决方案 可以尝试使用如下命令尝试解除锁定; 权限验证 可以使用如下命令进行验证权限; 经过以上的操作

    2024年01月17日
    浏览(83)
  • Docker安全基线检查需要修复的一些问题

    限制容器之间的网络流量 限制容器的内存使用量 为Docker启用内容信任 将容器的根文件系统挂载为只读 审核Docker文件和目录   默认情况下,同一主机上的容器之间允许所有网络通信。 如果不需要,请限制所有容器间的通信。 将需要相互通信的特定容器链接在一起。默认情况

    2024年01月18日
    浏览(32)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包