SimpleDateFormat 类的线程安全问题

1152人浏览 / 0人评论

参考

https://zhuanlan.zhihu.com/p/497704004?utm_id=0

https://www.cnblogs.com/cqqfboy/p/14497887.html

什么是内存泄漏

在 Java 中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在走向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为 Java 中的内存泄漏,这些对象不会被 GC 所回收,然而它却占用内存。

为什么 SimpleDateFormat 和 DateFormat 类不是线程安全的

SimpleDateFormat 继承了 DateFormat,在 DateFormat 中定义了一个 protected 属性的 Calendar 类的对象,然而 Calenda r内部并没有线程安全机制。

因此, SimpleDateFormat类不是线程安全的根本原因是:DateFormat 类中的 Calendar 对象被多线程共享,而 Calenda r对象本身不支持线程安全。

问题重现

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

/**
 * @author binghe
 * @version 1.0.0
 * @description 测试SimpleDateFormat的线程不安全问题
 */
public class SimpleDateFormatTest01 {
    //执行总次数
    private static final int EXECUTE_COUNT = 1000;
    //同时运行的线程数量
    private static final int THREAD_COUNT = 20;
    //SimpleDateFormat对象
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");

    public static void main(String[] args) throws InterruptedException {
        final Semaphore semaphore = new Semaphore(THREAD_COUNT);
        final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < EXECUTE_COUNT; i++){
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    try {
                        simpleDateFormat.parse("2020-01-01");
                    } catch (ParseException e) {
                        System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                        System.exit(1);
                    }catch (NumberFormatException e){
                        System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                        System.exit(1);
                    }
                    semaphore.release();
                } catch (InterruptedException e) {
                    System.out.println("信号量发生错误");
                    e.printStackTrace();
                    System.exit(1);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        System.out.println("所有线程格式化日期成功");
    }
}

问题解决

局部变量法

每次使用都 new 一个 SimpleDateFormat 对象。

当然,这种方式在高并发下会创建大量的 SimpleDateFormat 类对象,影响程序的性能,所以,这种方式在实际生产环境不太被推荐。

synchronized 锁方式

将 SimpleDateFormat 类对象定义成全局静态变量,此时所有线程共享 SimpleDateFormat 类对象,此时在调用格式化时间的方法时,对 SimpleDateFormat 对象进行同步即可,代码如下所示。

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

synchronized (simpleDateFormat){
	simpleDateFormat.parse("2020-01-01");
}

Lock 锁方式

private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
private static Lock lock = new ReentrantLock();

try {
	lock.lock();
	simpleDateFormat.parse("2020-01-01");
} catch (ParseException e) {
	System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
	e.printStackTrace();
	System.exit(1);
}catch (NumberFormatException e){
	System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
	e.printStackTrace();
	System.exit(1);
}finally {
	lock.unlock();
}

ThreadLocal 方式(推荐)

使用 ThreadLocal 存储每个线程拥有的 SimpleDateFormat 对象的副本,能够有效的避免多线程造成的线程安全问题。

private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){
	@Override
	protected DateFormat initialValue() {
		return new SimpleDateFormat("yyyy-MM-dd");
	}
};
// 可简化为:
private static final ThreadLocal<DateFormat> DATE_FORMATTER = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

// 使用
DATE_FORMATTER.get()

DateTimeFormatter 方式

DateTimeFormatter 是 Java8 提供的新的日期时间 API 中的类,DateTimeFormatter 类是线程安全的,可以在高并发场景下直接使用 DateTimeFormatter 类来处理日期的格式化操作。

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

LocalDate localDate = LocalDate.parse("2020-01-01", formatter);

joda-time 方式

joda-time 是第三方处理日期时间格式化的类库,是线程安全的。如果使用joda-time 来处理日期和时间的格式化,则需要引入第三方类库。

<dependency>
    <groupId>joda-time</groupId>
    <artifactId>joda-time</artifactId>
    <version>2.9.9</version>
</dependency>

示例:

import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

/**
 * @author binghe
 * @version 1.0.0
 * @description 通过DateTimeFormatter类解决线程安全问题
 */
public class SimpleDateFormatTest08 {
    //执行总次数
    private static final int EXECUTE_COUNT = 1000;
    //同时运行的线程数量
    private static final int THREAD_COUNT = 20;

    private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd");

    public static void main(String[] args) throws InterruptedException {
        final Semaphore semaphore = new Semaphore(THREAD_COUNT);
        final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < EXECUTE_COUNT; i++){
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    try {
                        DateTime.parse("2020-01-01", dateTimeFormatter).toDate();
                    }catch (Exception e){
                        System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                        System.exit(1);
                    }
                    semaphore.release();
                } catch (InterruptedException e) {
                    System.out.println("信号量发生错误");
                    e.printStackTrace();
                    System.exit(1);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        System.out.println("所有线程格式化日期成功");
    }
}

需要注意的是:DateTime 类是org.joda.time 包下的类,DateTimeFormat 类和 DateTimeFormatter 类都是org.joda.time.format包下的类。

import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

全部评论