在日常开发中,我们经常会用到时间,我们有很多办法在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方法的实现源码可以看出:
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。
将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("所有线程格式化日期成功");
}
}
将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("所有线程格式化日期成功");
}
将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("所有线程格式化日期成功");
}
使用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("所有线程格式化日期成功");
}
通过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类线程不安全问题会有新的认识。