【Java_基础深入】SimpleDateFormat.format()线程不安全的起因与解决方案

23 篇文章 2 订阅
5 篇文章 0 订阅

追踪问题

网上找到的最多讨论的就是 calendar的线程不安全操作传递到了SimpleDateFormat
针对Calendar进行断点观察,观察其值的变化

观察calendar的赋值链

在这里插入图片描述
初始化赋值为:calendar = Calendar.getInstance(TimeZone.getDefault(), loc);

关键方法:format()
    /**
     * Formats a Date into a date/time string.
     * @param date the time value to be formatted into a time string.
     * @return the formatted time string.
     */
    public final String format(Date date)
    {
        return format(date, new StringBuffer(),
                      DontCareFieldPosition.INSTANCE).toString();
    }

calendar.setTime(date)

private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {
        // Convert input date to time field list
        calendar.setTime(date); // 把时间设置进Calendar对象内,生成需要转换成String 的元数据
        // 省略其他分支代码
        switch (tag) {
            default:
                subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
                break;
            }
        }
        return toAppendTo;
}
  • 底层的subFormat() 大量进行calendar的读操作,这是线程不安全的核心
    在这里插入图片描述
解释线程不安全

A线程使用 SimpleDateFormat 对象 sa.format -> calendar.setTime(date) -> sa.subFormat()读calendar操作
线程A操作日期的调用链中,把对象sa暴露给了线程B线程B也执行了sa.format(date) -> calendar.setTime(date), 写操作 此时线程A在进行读calendar操作,calendar内部的date已经被修改了,就会操作预期外的结果。

线程B破坏了线程A的上下文变量calendar的属性值,造成了线程不安全的后果
属于未保证原子性的情况

解决方案

BUG背景

为了减少 new SimpleDateFormat 对象的操作, 把 SimpleDateFormat 对象进行不安全的发布,如非同步容器HashMap。是一种内存优化导致的bug

解决思路
  1. 减少对象创建,并保持并发能力 – 线程池
    使用Executors.newFixedThreadPool(10); 创建一个定容的线程池
    因为最大线程数 == 核心线程数, SimpleDateFormat 对象并发执行,不会抢夺对象
    只有一条线程释放了当前对象,下一个线程才有机会获得刚释放的对象,是一种无锁的操作,效率很高

  2. 线程隔离 – ThreadLocal
    第一次调用即初始化,节省内存。

    class ThreadSafeFormatterWithLambda {
       public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal
              = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
    }
    
  3. 综上
    同一时刻,最多只有10个线程,线程隔离得获取不同的SimpleDateFormat,并发格式化日期,
    效益较好,同时避免了线程安全问题

代码
/**
 * @Author james
 * @Description
 *
 * 多线程使用 SimpleDateFormat 场景下,使用线程池 + ThreadLocal
 *
 * @Date 2019/12/26
 */
public class DateFormatWithThreadLocal {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(()-> {
                String date = DateFormatWithThreadLocal.date(finalI);
                System.out.println(date);
            });
        }
        threadPool.shutdown();
    }

    public static String date(int seconds) {
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = ThreadSafeFormatterWithLambda.dateFormatThreadLocal.get();
        return dateFormat.format(date);
    }
}
class ThreadSafeFormatterWithLambda {
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal
            = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值