java中解决SimpleDateFormat类线程不安全问题

一、总述

在日常开发中,我们经常会用到时间,我们有很多办法在Java代码中获取时间,但我们最常用的方法就是使用SimpleDateFormat类。

SimpleDateFormat是Java提供的一个格式化和解析日期的工具类。它允许进行格式化(日期 -> 字符串)、解析(字符串 -> 日期)和规范化。SimpleDateFormat 使得可以选择任何用户定义的日期-时间格式的模式。

我们可以使用SimpleDateFormat的format方法,将一个Date类型转化成String类型,并且可以指定输出格式。

// Date转String
Date currentDate = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String currentDateStr = sdf.format(currentDate);
System.out.println(currentDateStr);

我们也可以使用SimpleDateFormat的parse方法,将一个String类型转化成Date类型。

System.out.println(sdf.parse(currentDateStr));

 

二、存在的安全隐患

SimpleDateFormat不是线程安全的根本原因是:DateFormat类中的Calendar对象被多线程共享,而Calendar对象本身不支持线程安全所以在多线程场景中,不能使用SimpleDateFormat作为共享变量。

具体原因,我们从SimpleDateFormat类中format方法的实现源码可以看出:

java中解决SimpleDateFormat类线程不安全问题_第1张图片

SimpleDateFormat中的format方法在执行过程中,会使用一个成员变量calendar来保存时间。如果我们在声明SimpleDateFormat的时候,使用的是static定义的。那么这个SimpleDateFormat就是一个共享变量,随之,SimpleDateFormat中的calendar也就可以被多个线程访问到。假设线程1刚刚执行完calendar.setTime把时间设置成2018-11-11,还没等执行完,线程2又执行了calendar.setTime把时间改成了2018-12-12。这时候线程1继续往下执行,拿到的calendar.getTime得到的时间就是线程2改过之后的2018-12-12。

三、如何解决

1、局部变量法

将SimpleDateFormat类对象定义成局部变量,就不会被多个线程同时访问到了,就避免了线程安全问题

package com.demo;
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;

public class SimpleDateFormatDemo {
    //程序执行总次数
    private static final int EXECUTE_TOTAL_COUNT = 1000;
    //程序同时运行的线程数量
    private static final int THREAD_TOTAL_COUNT = 20;
    private static SimpleDateFormat simpleDateFormat2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    //Lock对象
    private static Lock simpleDateFormatLock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        solution1();
    }

    /**
     *  1、局部变量法:将SimpleDateFormat类对象定义成局部变量
     * @throws InterruptedException
     */
    public static void solution1() throws InterruptedException{
        final Semaphore semaphore = new Semaphore(THREAD_TOTAL_COUNT);
        final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_TOTAL_COUNT);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < EXECUTE_TOTAL_COUNT; i++){
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    try {
                        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                        simpleDateFormat.parse("2024-01-28 11:00:00");
                    } catch (Exception e) {
                        System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                        System.exit(1);
                    }
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.exit(1);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        System.out.println("所有线程格式化日期成功");
    }
}

2、synchronized锁方式

将SimpleDateFormat类对象定义成全局静态变量,此时所有线程共享SimpleDateFormat类对象,通过加synchronized锁,使多个线程排队顺序执行。避免了并发导致的线程安全问题。

private static SimpleDateFormat simpleDateFormat2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
 * 2、synchronized锁方式:将SimpleDateFormat类对象定义成全局静态变量,此时所有线程共享SimpleDateFormat类对象,通过加锁,使多个线程排队顺序执行。避免了并发导致的线程安全问题。
 * @throws InterruptedException
 */
public static void solution2() throws InterruptedException{
    final Semaphore semaphore = new Semaphore(THREAD_TOTAL_COUNT);
    final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_TOTAL_COUNT);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < EXECUTE_TOTAL_COUNT; i++){
        executorService.execute(() -> {
            try {
                semaphore.acquire();
                try {
                    synchronized (simpleDateFormat2){
                        simpleDateFormat2.parse("2024-01-28 11:00:00");
                    }
                } catch (Exception e) {
                    System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                    e.printStackTrace();
                    System.exit(1);
                }
                semaphore.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.exit(1);
            }
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    executorService.shutdown();
    System.out.println("所有线程格式化日期成功");
}

3、Lock锁方式

将SimpleDateFormat类对象定义成全局静态变量,此时所有线程共享SimpleDateFormat类对象,通过加Lock锁,使多个线程排队顺序执行。避免了并发导致的线程安全问题。但是防止程序抛出异常而导致锁不能被释放,一定要将释放锁的操作放到finally代码块中。

//Lock对象
private static Lock simpleDateFormatLock = new ReentrantLock();
/**
 * 3、synchronized锁方式:将SimpleDateFormat类对象定义成全局静态变量,此时所有线程共享SimpleDateFormat类对象,通过加Lock锁,使多个线程排队顺序执行。避免了并发导致的线程安全问题。
 *  但是防止程序抛出异常而导致锁不能被释放,一定要将释放锁的操作放到finally代码块中。
 * @throws InterruptedException
 */
public static void solution3() throws InterruptedException{
    final Semaphore semaphore = new Semaphore(THREAD_TOTAL_COUNT);
    final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_TOTAL_COUNT);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < EXECUTE_TOTAL_COUNT; i++){
        executorService.execute(() -> {
            try {
                semaphore.acquire();
                try {
                    simpleDateFormatLock.lock();
                    simpleDateFormat2.parse("2024-01-28 11:00:00");
                } catch (Exception e) {
                    System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                    e.printStackTrace();
                    System.exit(1);
                }finally {
                    simpleDateFormatLock.unlock();
                }
                semaphore.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.exit(1);
            }
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    executorService.shutdown();
    System.out.println("所有线程格式化日期成功");
}

4、ThreadLocal方式

使用ThreadLocal存储每个线程拥有的SimpleDateFormat对象的副本,能够有效的避免多线程造成的线程安全问题。用 ThreadLocal 来实现其实是有点类似于缓存的思路,每个线程都有一个独享的对象,避免了频繁创建对象,也避免了多线程的竞争。

private static ThreadLocal simpleDateFormatThreadLocal = new ThreadLocal(){
    @Override
    protected DateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    }
};
/**
 * 4、ThreadLocal方式:使用ThreadLocal存储每个线程拥有的SimpleDateFormat对象的副本,能够有效的避免多线程造成的线程安全问题。
 * 用 ThreadLocal 来实现其实是有点类似于缓存的思路,每个线程都有一个独享的对象,避免了频繁创建对象,也避免了多线程的竞争。
 * @throws InterruptedException
 */
public static void solution4() throws InterruptedException{
    final Semaphore semaphore = new Semaphore(THREAD_TOTAL_COUNT);
    final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_TOTAL_COUNT);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < EXECUTE_TOTAL_COUNT; i++){
        executorService.execute(() -> {
            try {
                semaphore.acquire();
                try {
                    simpleDateFormatThreadLocal.get().parse("2024-01-28 11:00:00");
                } catch (Exception e) {
                    System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                    e.printStackTrace();
                    System.exit(1);
                }
                semaphore.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.exit(1);
            }
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    executorService.shutdown();
    System.out.println("所有线程格式化日期成功");
}

5、DateTimeFormatter方式

通过DateTimeFormatter类,因为DateTimeFormatter类是线程安全的,可以在高并发场景下直接使用DateTimeFormatter类来处理日期的格式化操作。

private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/**
 * 5、DateTimeFormatter方式:通过DateTimeFormatter类,因为DateTimeFormatter类是线程安全的,可以在高并发场景下直接使用DateTimeFormatter类来处理日期的格式化操作。
 * @throws InterruptedException
 */
public static void solution5() throws InterruptedException{
    final Semaphore semaphore = new Semaphore(THREAD_TOTAL_COUNT);
    final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_TOTAL_COUNT);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < EXECUTE_TOTAL_COUNT; i++){
        executorService.execute(() -> {
            try {
                semaphore.acquire();
                try {
                    LocalDate.parse("2024-01-28 11:00:00", formatter);
                } catch (Exception e) {
                    System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                    e.printStackTrace();
                    System.exit(1);
                }
                semaphore.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.exit(1);
            }
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    executorService.shutdown();
    System.out.println("所有线程格式化日期成功");
}

四、总结

在实际项目中使用SimpleDateFormat处理日期并没有那么简单,特别是高并发的环境下。通过这篇文章,相信大家对SimpleDateFormat类线程不安全问题会有新的认识。

你可能感兴趣的:(python,java,开发语言)