Java并发学习:不安全的SimpleDateFormat

SimpleDateFormat官方定义

SimpleDateFormat是一个具体的类,用于以区域设置敏感的方式格式化和解析日期。 它允许格式化(日期文本),解析(文本日期)和归一化。
SimpleDateFormat允许您从选择日期时间格式化的任何用户定义的模式开始。 不过,建议您创建一个日期-时间格式有两种getTimeInstance , getDateInstance ,或getDateTimeInstance在DateFormat 。 这些类方法中的每一个都可以返回使用默认格式模式初始化的日期/时间格式化程序。 您可以根据需要使用applyPattern方法修改格式模式。 有关使用这些方法的更多信息,请参见DateFormat 。

Synchronization
日期格式不同步。 建议为每个线程创建单独的格式实例。 如果多个线程同时访问格式,则必须在外部进行同步。

问题复现

官方既然说此类非线程安全,那么我们先来进行一下问题复现,看看到底会出什么问题。

package thread.juc.dateformat;

import java.text.ParseException;
import java.text.SimpleDateFormat;

/**
 * 多线程模式下SimpleDateFormat问题复现
 * @author jacklaile
 */
public class DateFormatProblem {

    // 创建日期格式化类
    static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        // 创建100个线程
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                try {
                    System.out.println(sdf.parse("2020-10-10 10:10:10"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

多次执行上述代码,可能会得到如下错误:

Exception in thread "Thread-801" 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:2084)
	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 thread.juc.dateformat.DateFormatProblem.lambda$main$0(DateFormatProblem.java:22)
	at java.lang.Thread.run(Thread.java:748)

进入SimpleDateFormat的实现方法parse(),看到如下代码:

Date parsedDate;
        try {
            parsedDate = calb.establish(calendar).getTime();
            // If the year value is ambiguous,
            // then the two-digit year == the default start year
            if (ambiguousYear[0]) {
                if (parsedDate.before(defaultCenturyStart)) {
                    parsedDate = calb.addYear(100).establish(calendar).getTime();
                }
            }
        }

这里的parsedDate = calb.establish(calendar).getTime();方法是线程不安全的,仔细查看establish(Calendar cal)方法,发现每次进行establish的时候,会调用cal.clear(),由于并非线程安全的,所以在多线程环境下,在线程使用cal对象很有可能已经被清空了,导致了异常。所以我们解决这个线程不安全的思路就是,需要保证每个线程使用自己的SimpleDateFormat对象。
下面来看看解决方法。

方法一

思路:每次使用时new一个SimpleDateFormat的实例,这样可以保证每个实例使用自己的Calendar实例,但是每次使用都需要new一个对象,并且使用后由于没有其他引用,又需要回收,开销会很大。
主要代码:

for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                try {
                    System.out.println(sdf.parse("2020-10-10 10:10:10"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
方法二

思路:由于SimpleDateFormat不是线程安全的,所以每次调用的时候对任务代码块进行加锁。

for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                try {
                    // 对SimpleDateFormat对象加锁,使得同时只有一个线程能够使用
                    synchronized (sdf) {
                        System.out.println(sdf.parse("2020-10-10 10:10:10"));
                    }
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }

不过这种加锁的方式就意味着监视器资源竞争,在高并发场景下依然低效。

方法三

思路:使用ThreadLocal,这样每个线程只需要使用一个SimpleDateFormat实例,这相比第一种方式大大节省了对象的创建销毁开销,并且不需要使多个线程同步。

ThreadLocal:对于ThreadLocal变量,每个访问ThreadLocal变量的线程会在本地生成一个变量的副本,当多线程操作这个变量的时候,操作的其实是自己本地的副本变量,不会影响其他线程,提高了效率。

根据上面的思路,对代码进行改造。

package thread.juc.dateformat;

import java.text.ParseException;
import java.text.SimpleDateFormat;

/**
 * 多线程模式下SimpleDateFormat问题复现
 * 使用ThreadLocal解决并发效率低下的问题
 * @author jacklaile
 */
public class SafeDateFormat3 {

    // 创建日期格式化类
    static ThreadLocal<SimpleDateFormat> localSdf = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static void main(String[] args) {
        // 创建1000个线程
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                try {
                    // 得到的是每个线程自己本地的变量副本
                    System.out.println(localSdf.get().parse("2020-10-10 10:10:10"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

参考资料:
《Java并发编程之美》

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值