首页 > 其他分享 >SimpleDateFormat线程安全问题探究

SimpleDateFormat线程安全问题探究

时间:2023-08-03 19:34:34浏览次数:33  
标签:01 java 18 SimpleDateFormat parse 探究 线程


目录

一. 问题现象

二. 原因排查

三. 原因分析

四. 解决方案


一. 问题现象

运营部门反馈使用小程序配置的拉新现金红包活动二维码,在扫码后跳转至404页面。

二. 原因排查

1、首先,检查扫码后的跳转链接地址不是对应二维码的实际URL,根据代码逻辑推测,可能是accessToken在微信端已失效导致,检查数据发现,数据库存储的accessToken过期时间为2022-11-29(排查问题当日为2022-10-08),发现过期时间太长,导致accessToken未刷新导致。

2、接下来,继续排查造成这一问题的真正原因。排查日志发现更新sql语句对应的的过期时间与数据库记录的一致,推测赋值代码存在问题,如下。

tokenInfo.setExpireTime(simpleDateFormat.parse(token.getString("expireTime")));

其中,simpleDateFormat在代码中定义是该类的成员变量。

 3、跟踪代码后发现源码中有明确说明SimpleDateFormat不应该应用于多线程场景下。

Synchronization
//SimpleDateFormat中的日期格式化不是同步的。
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.

4、至此,基本可以判断是simpleDateFormat.parse在多线程情况下造成错误的过期时间入库,导致accesstoken无法正常更新。

三. 原因分析

1、接下来写个测试类来模拟:

@RunWith(SpringRunner.class)
@SpringBootTest
public class SimpleDateFormatTest {

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

    /**
     * 定义线程池
     **/
    private static final ExecutorService threadPool = new ThreadPoolExecutor(16,
            20,
            0L,
            TimeUnit.MILLISECONDS,
            new LinkedBlockingDeque<>(1024),
            new ThreadFactoryBuilder().setNamePrefix("[线程]").build(),
            new ThreadPoolExecutor.AbortPolicy()
    );

    @SneakyThrows
    @Test
    public void testParse() {
        Set<String> results = Collections.synchronizedSet(new HashSet<>());
        // 每个线程都对相同字符串执行“parse日期字符串”的操作,当THREAD_NUMBERS个线程执行完毕后,应该有且仅有一个相同的结果才是正确的
        String initialDateStr = "2022-10-08 18:30:01";
        for (int i = 0; i < 20; i++) {
            threadPool.execute(() -> {
                Date parse = null;
                try {
                    parse = simpleDateFormat.parse(initialDateStr);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "---" + parse);
            });
        }
        threadPool.shutdown();
        threadPool.awaitTermination(1, TimeUnit.HOURS);
    }
}

2、运行结果如下:

[线程]5---Sat Jan 08 18:30:01 CST 2000
[线程]0---Wed Oct 08 18:30:01 CST 2200
[线程]4---Sat Oct 08 18:30:01 CST 2022
Exception in thread "[线程]3" 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:1869)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    at java.text.DateFormat.parse(DateFormat.java:364)
    at com.SimpleDateFormatTest.lambda$testParse$0(SimpleDateFormatTest.java:49)
    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)
[线程]6---Sat Oct 08 18:30:01 CST 2022
[线程]11---Wed Mar 15 18:30:01 CST 2045
Exception in thread "[线程]2" java.lang.ArrayIndexOutOfBoundsException: 275
    at sun.util.calendar.BaseCalendar.getCalendarDateFromFixedDate(BaseCalendar.java:453)
    at java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2397)
    at java.util.GregorianCalendar.computeTime(GregorianCalendar.java:2818)
    at java.util.Calendar.updateTime(Calendar.java:3393)
    at java.util.Calendar.getTimeInMillis(Calendar.java:1782)
    at java.util.Calendar.getTime(Calendar.java:1755)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1532)
    at java.text.DateFormat.parse(DateFormat.java:364)
    at com.SimpleDateFormatTest.lambda$testParse$0(SimpleDateFormatTest.java:49)
    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)
[线程]6---Fri Oct 01 18:30:01 CST 8202
[线程]12---Sat Oct 08 18:30:01 CST 2022
Exception in thread "[线程]1" 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:1869)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    at java.text.DateFormat.parse(DateFormat.java:364)
    at com.SimpleDateFormatTest.lambda$testParse$0(SimpleDateFormatTest.java:49)
    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)
[线程]0---Sat Oct 08 18:30:01 CST 2022
[线程]12---Sat Oct 08 18:30:01 CST 2022
[线程]13---Sat Oct 08 18:30:01 CST 2022
[线程]18---Sat Oct 08 18:30:01 CST 2022
[线程]6---Sat Oct 01 18:30:01 CST 2022
[线程]7---Sat Oct 08 18:30:01 CST 2022
[线程]10---Sat Oct 08 18:30:01 CST 2022
[线程]15---Sat Oct 08 18:00:01 CST 2022
[线程]17---Sat Oct 08 18:30:01 CST 2022
[线程]14---Sat Oct 08 18:30:01 CST 2022
预期结果个数 1---实际结果个数7

不仅有的线程结果不正确,甚至还有一些线程还出现了异常!

3、为什么SimpleDateFormat类不是线程安全的?

SimpleDateFormat继承了DateFormat,DateFormat内部有一个Calendar对象的引用,主要用来存储和SimpleDateFormat相关的日期信息。

SimpleDateFormat对parse()方法的实现。

关键代码如下:

@Override
public Date parse(String text, ParsePosition pos) {
    ...省略中间代码
    Date parsedDate;
    try {
        ...
        parsedDate = calb.establish(calendar).getTime();
    } catch (IllegalArgumentException e) {
       ...
    }

    return parsedDate;
}

establish()的实现如下:

Calendar establish(Calendar cal) {
    ...省略中间代码
    cal.clear();
    for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
        for (int index = 0; index <= maxFieldIndex; index++) {
            if (field[index] == stamp) {
                cal.set(index, field[MAX_FIELD + index]);
                break;
            }
        }
    }
    ...
    return cal;
}

在多个线程共享SimpleDateFormat时,同时也共享了Calendar引用,在如上代码中,calendar首先会进行clear()操作,然后进行set操作,在多线程情况下,set操作会覆盖之前的值,而且在后续对日期进行操作时,也可能会因为clear操作被清除导致异常。

四. 解决方案

1、将SimpleDateFormat定义成局部变量,每次使用时都new一个新对象,频繁创建对象消耗大,性能影响一些(JDK文档推荐此做法)

public static Date parse(String strDate) throws ParseException {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    return sdf.parse(strDate);
}

2、维护一个SimpleDateFormat实体,转换方法上使用 Synchronized 保证线程安全:多线程堵塞(并发大系统不推荐) 

private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
      
public static String formatDate(Date date)throws ParseException{
    synchronized(sdf){
        return sdf.format(date);
    }  
}
    
public static Date parse(String strDate) throws ParseException{
    synchronized(sdf){
        return sdf.parse(strDate);
    }
}

3、使用ThreadLocal : 线程独享不堵塞,并且减少创建对象的开销(如果对性能要求比较高的情况,推荐这种方式)。 

public static ThreadLocal<DateFormat> threadLocal = ThreadLocal.withInitial(
    () -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
);

public static Date parse(String strDate) throws ParseException {
    return threadLocal.get().parse(strDate);
}

 4、DateTimeFormatter是Java8提供的新的日期时间API中的类,DateTimeFormatter类是线程安全的,可以在高并发场景下直接使用。

String dateTimeStr= "2016-10-25 12:00:00";
    DateTimeFormatter formatter02 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    LocalDateTime localDateTime = LocalDateTime.parse(dateTimeStr,formatter02);
    System.out.println(localDateTime);
    String format = localDateTime.format(formatter02);
    System.out.println(format);

    2016-10-25T12:00
    2016-10-25 12:00:00

最终,我们根据实际情况公共包DateUtil类提供的strConvertDate方法,原理是按照方案1来解决该问题。

标签:01,java,18,SimpleDateFormat,parse,探究,线程
From: https://blog.51cto.com/u_13693015/6952297

相关文章

  • [async]子线程内开启协程 RuntimeError: There is no current event loop in thread '
    在子线程内直接获取事件循环会报错:RuntimeError:Thereisnocurrenteventloopinthread'Thread-2',此时的代码为:loop=asyncio.get_event_loop()loop.run_until_complete(协程函数) #执行解决方法:在子线程内创建并配置事件循环new_loop=asyncio.new_event_loop(......
  • 进程,线程和协程;为什么有了GIL锁还要互斥锁;多态和多态性;鸭子类型
    进程,线程和协程;为什么有了GIL锁还要互斥锁;多态和多态性;鸭子类型为什么有了GIL锁还要互斥锁1.GIL本身就是一个大的互斥锁2.同一个进程下资源是共享的,也就是说多条线程可以操作同一个变量3.多个线程可以操作同一个变量就会出现数据安全问题4.临界区:指一段代码或一段程序片段,需......
  • 面试官:线程是如何通讯的?
    线程通信是指多个线程之间通过某种机制进行协调和交互,例如,线程等待和通知机制就是线程通讯的主要手段之一。在Java中,线程等待和通知的实现手段有以下几种方式:Object类下的wait()、notify()和notifyAll()方法;Condition类下的await()、signal()和signalAll()方法;Lo......
  • pytest 中添加线程
    #D:\pythonProject0726\test_case\test_one.pyimporttimedefsetup_module():print('准备测试数据')defteardown_module():print('清理测试数据')deftest_one():ex=1real=1time.sleep(3)print('1==1')assert......
  • 8 个线程池最佳实践和坑!使用不当直接生产事故!!
    这篇文章我会简单总结一下我了解的使用线程池的时候应该注意的坑以及一些优秀的实践。拿来即用,美滋滋!内容概览:Java指南:https://java-family.cn1、正确声明线程池线程池必须手动通过 ThreadPoolExecutor 的构造函数来声明,避免使用Executors 类创建线程池,会有OOM风险。Ex......
  • 【Java】多线程面试题总结
    最近在看面试题,所以想用自己的理解总结一下,便于加深印象。为什么使用多线程使用多线程可以充分利用CPU,提高CPU的使用率。提高系统的运行效率,对于一些复杂或者耗时的功能,可以对其进行拆分,比如将某个任务拆分了A、B、C三个子任务,如果子任务之间没有依赖关系,那么就可以使用多线程......
  • Java面试题 P42:框架篇:Spring-Spring框架中的单例bean是线程安全的吗?Spring框架中的bea
        ......
  • 为什么有了gil锁还要互斥锁、 进程,线程和协程 、什么是鸭子类型
    目录1为什么有了gil锁还要互斥锁互斥锁保证数据安全2进程,线程和协程在哪用过3什么是鸭子类型1为什么有了gil锁还要互斥锁gil:全局解释器锁,线程要执行,必须先获得到gil锁,才能执行互斥锁:为了保证多线程并发操作数据(变量)而设置的锁,保证在加锁和释放锁之间,其他线程不能操作gi......
  • 3 Linux多线程开发
    3Linux多线程开发3.1线程概述3.1.1线程概述与进程(process)类似,线程(thread)是允许应用程序并发执行多个任务的一种机制。一个进程可以包含多个线程。同一个程序中的所有线程均会独立执行相同程序,且共享同一份全局内存区域,其中包括初始化数据段、未初始化数据段,以及堆内存段。(......
  • 【Jmeter】跨线程获取cookie值
    1、设置配置文件:apache-jmeter-5.5\bin\jmeter.properties#CookieManagerbehaviour-shouldCookiesbestoredasvariables?#DefaultisfalseCookieManager.save.cookies=true2、线程_登录:2.1、http请求右键_添加_后置处理器_正则表达式提取器 正则表达式:satoken=(......