java 日期加上几天_【优雅的避坑】不安全!别再共享SimpleDateFormat变量了日期时间处理的正确姿势...

0x01 开场白

JDK文档中已经明确表明了「SimpleDateFormat不应该用在多线程场景」中:

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.

然而,并不是所有Javaer都关注到了这句话,依然使用如下的方式进行日期时间格式化:

private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

@Test
public void longLongAgo() {
    String dateStr = sdf.format(new Date());
    System.out.println("当前时间:" + dateStr);
}

一个线程这样做当然是没问题的。

既然官方文档都说了在多线程访问的场景中必须使用「synchronized同步」,那么就来验证一下,多线程场景下使用SimpleDateFormat会出现什么问题。

0x02 重现多线程场景使用SimpleDateFormat问题

定义一个线程池,跑多个线程执行「对当前日期格式化」的操作

/**
 * 定义static的SimpleDateFormat,所有线程共享
 **/
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

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

@SneakyThrows
@Test
public void testFormat() {
    Set results = Collections.synchronizedSet(new HashSet<>());// 每个线程都执行“给日期加上一个天数”的操作,每个线程加的天数均不一样,// 这样当THREAD_NUMBERS个线程执行完毕后,应该有THREAD_NUMBERS个结果才是正确的for (int i = 0; i         Calendar calendar = Calendar.getInstance();int addDay = i;
        threadPool.execute(() -> {
            calendar.add(Calendar.DATE, addDay);
            String result = sdf.format(calendar.getTime());
            results.add(result);
        });
    }//保证线程执行完
    threadPool.shutdown();
    threadPool.awaitTermination(1, TimeUnit.HOURS);//最后打印结果
    System.out.println("希望:" + THREAD_NUMBERS + ",实际:" + results.size());
}
d9028949f54047e9618df4cdb93fd26a.png

正常情况下,以上代码results.size()的结果应该是THREAD_NUMBERS。但是实际执行结果是一个小于该值的数字。

上面是format()方法出现的问题,同样,SimpleDateFormatparse()方法也会出现线程不安全的问题:

@SneakyThrows
@Test
public void testParse() {
    String dateStr = "2020-10-22 08:08:08";
    for (int i = 0; i 20; i++) {
        threadPool.execute(() -> {
            try {
                Date date = sdf.parse(dateStr);
                System.out.println(Thread.currentThread().getName() + "---" + date);
            } catch (ParseException e) {
                e.printStackTrace();
            }
        });
    }
    //保证线程执行完
    threadPool.shutdown();
    threadPool.awaitTermination(1, TimeUnit.HOURS);
}

运行结果:

[线程-0]---Thu May 22 08:00:08 CST 2228
[线程-3]---Sun Oct 22 08:08:08 CST 8000
[线程-4]---Thu Oct 22 08:08:08 CST 2020
[线程-5]---Thu Oct 22 08:08:08 CST 2020
Exception in thread "[线程-1]" Exception in thread "[线程-2]" java.lang.NumberFormatException: For input string: "101.E1012E2"
 at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
 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:2056)
 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.xblzer.tryout.SimpleDateFormatTest.lambda$testParse$1(SimpleDateFormatTest.java:78)
 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: For input string: "101.E1012E2"
 at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
 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:2056)
 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.xblzer.tryout.SimpleDateFormatTest.lambda$testParse$1(SimpleDateFormatTest.java:78)
 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)
[线程-8]---Wed Jan 22 08:09:28 CST 2020
[线程-11]---Sat Jan 25 16:08:08 CST 2020
[线程-9]---Thu Oct 22 08:08:08 CST 2020
Exception in thread "[线程-12]" java.lang.NumberFormatException: For input string: ""
[线程-10]---Thu Oct 22 08:08:08 CST 2020
 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:2051)
 at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
[线程-13]---Thu Oct 22 08:08:08 CST 2020
 at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
 at java.text.DateFormat.parse(DateFormat.java:364)
 at com.xblzer.tryout.SimpleDateFormatTest.lambda$testParse$1(SimpleDateFormatTest.java:78)
 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
[线程-14]---Thu Oct 22 08:08:08 CST 2020
 at java.lang.Thread.run(Thread.java:748)
[线程-16]---Thu Oct 22 08:08:08 CST 2020
[线程-18]---Thu Oct 22 08:08:08 CST 2020
[线程-16]---Thu Oct 22 08:08:08 CST 2020
[线程-18]---Thu Oct 22 08:08:08 CST 2020
Exception in thread "[线程-0]" java.lang.NumberFormatException: For input string: ""
[线程-16]---Thu Oct 22 08:08:08 CST 2020
[线程-17]---Thu Oct 22 08:08:08 CST 2020
 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:2051)
 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.xblzer.tryout.SimpleDateFormatTest.lambda$testParse$1(SimpleDateFormatTest.java:78)
 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)

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

0x03 原因分析

原因就是因为 SimpleDateFormat 作为一个非线程安全的类,被当做了static「共享变量」在多个线程中进行使用,这就出现了「线程安全问题」

来跟一下源码。

format(Date date)方法来源于类DateFormat中的如下方法:

public final String format(Date date){
    return format(date, new StringBuffer(),
                  DontCareFieldPosition.INSTANCE).toString();
}

调用abstract StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition)

public abstract StringBuffer format(Date date, StringBuffer toAppendTo,
                                        FieldPosition fieldPosition);

这是一个抽象方法,具体的实现看SimpleDateFormat类中的实现:

// Called from Format after creating a FieldDelegate
private StringBuffer format(Date date, StringBuffer toAppendTo,
                            FieldDelegate delegate) {
    // Convert input date to time field list
    calendar.setTime(date);

    boolean useDateFormatSymbols = useDateFormatSymbols();

    for (int i = 0; i         int tag = compiledPattern[i] >>> 8;
        int count = compiledPattern[i++] & 0xff;
        if (count == 255) {
            count = compiledPattern[i++] <16;
            count |= compiledPattern[i++];
        }

        switch (tag) {
        case TAG_QUOTE_ASCII_CHAR:
            toAppendTo.append((char)count);
            break;

        case TAG_QUOTE_CHARS:
            toAppendTo.append(compiledPattern, i, count);
            i += count;
            break;

        default:
            subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
            break;
        }
    }
    return toAppendTo;
}

大家看到了吧,format方法在执行过程中,会使用一个成员变量calendar来保存时间。这就是问题的关键所在。

由于我们在声明SimpleDateFormat sdf的时候,使用的是static 定义的,所以这个sdf就是一个共享的变量,那么SimpleDateFormat中的calendar也可以被多个线程访问到。

例如,[线程-1]刚刚执行完calendar.setTime 把时间设置成 2020-10-22,还没执行完呢,[线程-2]又执行了calendar.setTime把时间改成了 2020-10-23。此时,[线程-1]继续往下执行,执行calendar.getTime得到的时间就是[线程-2]改过之后的。也就是说[线程-1]的setTime的结果被无情的无视了...

0x04 日期格式化的正确姿势

姿势1 使用synchronized

synchronized对共享变量加同步锁,使多个线程排队按照顺序执行,从而避免多线程并发带来的线程安全问题。

@SneakyThrows
@Test
public void testWithSynchronized() {
    Set results = Collections.synchronizedSet(new HashSet<>());for (int i = 0; i         Calendar calendar = Calendar.getInstance();int addDays = i;
        threadPool.execute(() -> {synchronized (sdf) {
                calendar.add(Calendar.DATE, addDays);
                String result = sdf.format(calendar.getTime());//System.out.println(Thread.currentThread().getName() + "---" + result);
                results.add(result);
            }
        });
    }//保证线程执行完
    threadPool.shutdown();
    threadPool.awaitTermination(1, TimeUnit.HOURS);//最后打印结果
    System.out.println("希望:" + THREAD_NUMBERS + ",实际:" + results.size());
}
0b4f3b9b64d22baefb9c2f7c950673ac.png

姿势2 将SimpleDateFormat设置成局部变量使用

「局部变量不会被多个线程共享」,也可以避免线程安全问题。

@SneakyThrows
@Test
public void testWithLocalVar() {
    Set results = Collections.synchronizedSet(new HashSet<>());for (int i = 0; i         Calendar calendar = Calendar.getInstance();int addDays = i;
        threadPool.execute(() -> {
            SimpleDateFormat localSdf = new SimpleDateFormat("yyyy-MM-dd");
            calendar.add(Calendar.DATE, addDays);
            String result = localSdf.format(calendar.getTime());//System.out.println(Thread.currentThread().getName() + "---" + result);
            results.add(result);
        });
    }//保证线程执行完
    threadPool.shutdown();
    threadPool.awaitTermination(1, TimeUnit.HOURS);//最后打印结果
    System.out.println("希望:" + THREAD_NUMBERS + ",实际:" + results.size());
}

每个线程都定义自己的变量SimpleDateFormat localSdf,格式化localSdf.format(calendar.getTime()),不会有线程安全问题。

e596fb9bec257c016f2832cc634e3366.png

姿势3 使用ThreadLocal

ThreadLocal的目的是确保每个线程都可以得到一个自己的 SimpleDateFormat的对象,所以也不会出现多线程之间的竞争问题。

/**
 * 定义线程数量
 **/
private static final int THREAD_NUMBERS = 50;

/**
 * 定义ThreadLocal,每个线程都有一个独享的对象
 **/
private static ThreadLocal dateFormatThreadLocal = new ThreadLocal<>();/**
 * 定义线程池
 **/private static ExecutorService threadPool = new ThreadPoolExecutor(16,100,0L,
        TimeUnit.MILLISECONDS,new LinkedBlockingDeque<>(1024),new ThreadFactoryBuilder().setNameFormat("[线程-%d]").build(),new ThreadPoolExecutor.AbortPolicy()
        );/**
 * 延迟加载SimpleDateFormat
 **/private static SimpleDateFormat getDateFormat() {
    SimpleDateFormat dateFormat = dateFormatThreadLocal.get();if (dateFormat == null) {
        dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        dateFormatThreadLocal.set(dateFormat);
    }return dateFormat;
}@SneakyThrows@Testpublic void testFormatWithThreadLocal() {
    Set results = Collections.synchronizedSet(new HashSet<>());// 每个线程都执行“给日期加上一个天数”的操作,每个线程加的天数均不一样,// 这样当THREAD_NUMBERS个线程执行完毕后,应该有THREAD_NUMBERS个结果才是正确的for (int i = 0; i         Calendar calendar = Calendar.getInstance();int addDay = i;
        threadPool.execute(() -> {
            calendar.add(Calendar.DATE, addDay);//获取ThreadLocal中的本地SimpleDateFormat副本
            String result = getDateFormat().format(calendar.getTime());
            results.add(result);
        });
    }//保证线程执行完
    threadPool.shutdown();
    threadPool.awaitTermination(1, TimeUnit.HOURS);//最后打印结果
    System.out.println("希望:" + THREAD_NUMBERS + ",实际:" + results.size());
}

关键点就是

getDateFormat().format(calendar.getTime());

getDateFormat()拿到属于自己线程的SimpleDateFormat对象。

运行结果:

9b3765f266468b843d2c414cfc31f9df.png

姿势4 使用DateTimeFormatter

Java 8之后,JDK提供了DateTimeFormatter类:

32288435c209a69f8d8eafa86cc29077.png
DateTimeFormatter

它也可以进行事件、日期的格式化,并且它是「不可变的、线程安全的」

结合Java 8的LocalDateTime时间操作工具类进行测试验证:

Java 8的LocalDate、LocalTime、LocalDateTime进一步加强了对日期和时间的处理。

/**
 * 定义线程数量
 **/
private static final int THREAD_NUMBERS = 50;

private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");

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

@SneakyThrows
@Test
public void testDateTimeFormatter() {
    Set results = Collections.synchronizedSet(new HashSet<>());for (int i = 0; i         //这样写为了能用Lambda表达式
        LocalDateTime[] now = {LocalDateTime.now()};int addDay = i;
        threadPool.execute(() -> {
            now[0] = now[0].plusDays(addDay);//System.out.println(Thread.currentThread().getName() + "====" + now[0]);
            String result = now[0].format(formatter);
            results.add(result);
        });
    }
    threadPool.shutdown();
    threadPool.awaitTermination(1, TimeUnit.HOURS);
    System.out.println("希望:" + THREAD_NUMBERS + ",实际:" + results.size());
}

结果验证:

4737b0f59215eec4007e24b1f8a8d80c.png

小结

SimpleDateFormat存在线程安全问题,使用以下几种方式解决该问题。

  • 加synchronized同步锁。并发量大的时候会有性能问题,线程阻塞。
  • 将SimpleDateFormat设置为局部变量。会频繁的创建和销毁对象,性能较低。
  • 使用ThreadLocal。推荐使用。
  • 使用Java 8新特性DateTimeFormatter。推荐使用。

往期推荐

【优雅的避坑】避免HashMap扩容的正确姿势

【优雅的避坑】你的钱算错了!为什么0.1+0.2不等于0.3了!?

【优雅的避坑】不要轻易使用==比较两个Integer的值

优雅的避坑-从验证码功能代码优化到JVM栈和堆

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值