大家好,今天我们一起聊聊Java开发必须知道的日期处理。
大纲

SimpleDateFormat处理日期
SimpleDateFormat是处理日期的常用工具类,在日常开发中,到处可见,以其简单易用、灵活方便、功能丰富,而被广泛使用。但SimpleDateFormat的使用,要看应用场景,在高并发的场景下使用,要非常留意谨慎,因为它线程不安全。
SimpleDateFormat线程不安全
看到SimpleDateFormat线程不安全,你可能会说,哥们我一直都是这么用SimpleDateFormat来处理日期的,也是在高并发的场景下使用的,怎么从来没有出现过线程不安全的报错?经过实践检验,它是线程安全的。
为什么会有这个错觉?那是因为,你的高并发场景,并非达到高并发的量,在高并发场景下,确实会存在线程不安全。
SimpleDateFormat线程不安全的原因
SimpleDateFormat派生自DateFormat,DateFormat维护了一个全局变量Calender:
/**
* The {@link Calendar} instance used for calculating the date-time fields
* and the instant of time. This field is used for both formatting and
* parsing.
*
* <p>Subclasses should initialize this field to a {@link Calendar}
* appropriate for the {@link Locale} associated with this
* <code>DateFormat</code>.
* @serial
*/
protected Calendar calendar;
从注释可以看出,calendar既用于日期格式化,也用于日期解析。我们知道,全局变量若只读不变化,则该全局变量不会存在线程安全问题;当全局变量的值可能发生变化,而又会被读取使用时,就存在线程安全问题了。那么是否线程安全,取决于使用过程中,是否存在修改的可能。
@Override
public Date parse(String text, ParsePosition pos)
{
//为简化代码,此处省调若干行不想管的代码,仅保留部分关键代码
CalendarBuilder calb = new CalendarBuilder();
Date parsedDate;
try {
parsedDate = calb.establish(calendar).getTime();
}
// An IllegalArgumentException will be thrown by Calendar.getTime()
// if any fields are out of range, e.g., MONTH == 17.
catch (IllegalArgumentException e) {
pos.errorIndex = start;
pos.index = oldStart;
return null;
}
return parsedDate;
}
以上代码,不存在对全局变量calender的修改操作,那么我们看看calb调用的方法establish(calender)。
Calendar establish(Calendar cal) {
boolean weekDate = isSet(WEEK_YEAR)
&& field[WEEK_YEAR] > field[YEAR];
if (weekDate && !cal.isWeekDateSupported()) {
// Use YEAR instead
if (!isSet(YEAR)) {
set(YEAR, field[MAX_FIELD + WEEK_YEAR]);
}
weekDate = false;
}
cal.clear();
// Set the fields from the min stamp to the max stamp so that
// the field resolution works in the Calendar.
for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
for (int index = 0; index <= maxFieldIndex; index++) {
if (field[index] == stamp) {
cal.set(index, field[MAX_FIELD + index]);
break;
}
}
}
if (weekDate) {
int weekOfYear = isSet(WEEK_OF_YEAR) ? field[MAX_FIELD + WEEK_OF_YEAR] : 1;
int dayOfWeek = isSet(DAY_OF_WEEK) ?
field[MAX_FIELD + DAY_OF_WEEK] : cal.getFirstDayOfWeek();
if (!isValidDayOfWeek(dayOfWeek) && cal.isLenient()) {
if (dayOfWeek >= 8) {
dayOfWeek--;
weekOfYear += dayOfWeek / 7;
dayOfWeek = (dayOfWeek % 7) + 1;
} else {
while (dayOfWeek <= 0) {
dayOfWeek += 7;
weekOfYear--;
}
}
dayOfWeek = toCalendarDayOfWeek(dayOfWeek);
}
cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek);
}
return cal;
}
细心的你,是否已经看到了,该方法中,存在对cal的变量的修改操作,cal.clear()和cal.setWeekDate(),也就是先清楚cal的值,再重新设置该值。由于Calender内部没有安全处理机制,且这两步操作,也没有进行加锁等特殊处理等,所以在多线程操作同一个对象时,会存在Calendar对象值存在混乱的情况,即会出现线程不安全。
SimpleDateFormat线程不安全验证
话不多说,代码撸起
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
private static final int thdNum = 50;
private static final int executeCount = 10000;
@Test
public void testSimDateFormatNotSafe() throws InterruptedException {
final Semaphore semaphore = new Semaphore(thdNum);
final CountDownLatch countDownLatch = new CountDownLatch(executeCount);
ExecutorService executor = Executors.newCachedThreadPool();
for(int i=0; i< executeCount; i++) {
executor.submit(() -> {
try {
semaphore.acquire();
try {
sdf.parse("2024-01-01");
} catch (Exception e) {
System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
System.exit(-1);
}
semaphore.release();
} catch (InterruptedException e) {
System.out.println("信号量发生錯誤");
e.printStackTrace();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executor.shutdown();
System.out.println("所有线程格式化日期成功");
}
栗子中,使用50个线程,每个线程进行10000次操作。运行结果如下:
线程:pool-1-thread-8 格式化日期失败
线程:pool-1-thread-16 格式化日期失败
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.flycloud.test.datetime.SimpleDateFormatTest.lambda$testSimDateFormatNotSafe$0(SimpleDateFormatTest.java:38)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
java.lang.NumberFormatException: empty String
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
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 com.flycloud.test.datetime.SimpleDateFormatTest.lambda$testSimDateFormatNotSafe$0(SimpleDateFormatTest.java:38)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
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:2051)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.flycloud.test.datetime.SimpleDateFormatTest.lambda$testSimDateFormatNotSafe$0(SimpleDateFormatTest.java:38)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
进程已结束,退出代码 -1
以上栗子可以看出,SimpleDateFormat在多线程处理时,出现了异常报错,确实存在线程不安全的现象。
如何让SimpleDateFormat线程安全
有多种方式可以让SimpleDateFormat线程安全,我们分别举例说明。
使用局部变量
局部变量,方法内使用,当然是线程安全的。代码撸起:
@Test
public void testSimDateFormatByLocalParam() throws InterruptedException {
final Semaphore semaphore = new Semaphore(thdNum);
final CountDownLatch countDownLatch = new CountDownLatch(executeCount);
ExecutorService executor = Executors.newCachedThreadPool();
for(int i=0; i< executeCount; i++) {
executor.submit(() -> {
try {
semaphore.acquire();
try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.parse("2024-01-01");
} catch (Exception e) {
System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
System.exit(-1);
}
semaphore.release();
} catch (InterruptedException e) {
System.out.println("信号量发生錯誤");
e.printStackTrace();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executor.shutdown();
System.out.println("所有线程格式化日期成功");
}
结果如下:
所有线程格式化日期成功
进程已结束,退出代码 0
使用此方式,可以解决SimplDateFormat的线程安全问题,但又带来另一个问题,频繁的创建SimpleDateFormat对象,会增加gc的频次,高并发场景下,我们也需要考虑最大限度的降低gc的次数。不推荐使用。
Sync同步锁
既然全局变量在修改的情况下,存在线程不安全,那么我们是否可以对其进行加锁操作,确保在使用的时候只有一个线程修改?继续撸代码:
@Test
public void testSimDateFormatBySync() throws InterruptedException {
final Semaphore semaphore = new Semaphore(thdNum);
final CountDownLatch countDownLatch = new CountDownLatch(executeCount);
ExecutorService executor = Executors.newCachedThreadPool();
for(int i=0; i< executeCount; i++) {
executor.submit(() -> {
try {
semaphore.acquire();
try {
synchronized (sdf) {
sdf.parse("2024-01-01");
}
} catch (Exception e) {
System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
System.exit(-1);
}
semaphore.release();
} catch (InterruptedException e) {
System.out.println("信号量发生錯誤");
e.printStackTrace();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executor.shutdown();
System.out.println("所有线程格式化日期成功");
}
在调用sdf.parse()时进行synchronized加锁操作,这是多线程处理时,解决线程安全的最基本的操作,看结果:
所有线程格式化日期成功
进程已结束,退出代码 0
可以从结果看出,经过synchronized加锁后,得到了预期的结果:线程安全。但把本来可以多线程并发处理的并行操作,强制变成了串行操作,这对系统的吞吐量是有损害的,同时,也不符合高并发处理的基本原则。不推荐使用。
Lock同步锁
同Synchronize类似,采用另一种加锁的方式。代码如下:
@Test
public void testSimDateFormatByLock() throws InterruptedException {
final Semaphore semaphore = new Semaphore(thdNum);
final CountDownLatch countDownLatch = new CountDownLatch(executeCount);
Lock lock = new ReentrantLock();
ExecutorService executor = Executors.newCachedThreadPool();
for(int i=0; i< executeCount; i++) {
executor.submit(() -> {
try {
semaphore.acquire();
try {
lock.lock();
sdf.parse("2024-01-01");
} catch (Exception e) {
System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
System.exit(-1);
}finally{
lock.unlock();
}
semaphore.release();
} catch (InterruptedException e) {
System.out.println("信号量发生錯誤");
e.printStackTrace();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executor.shutdown();
System.out.println("所有线程格式化日期成功");
}
lock加锁的写法与Synchronize有所不同,但效果是一样的,也存在高并发由并行强制为串行的操作。不推荐。
所有线程格式化日期成功
进程已结束,退出代码 0
线程独占SimpleDateFormat对象
既然使用加锁的方式,不管是Synchronize或者lock方式,均违法高并发处理的初衷,那是否可以换一种思路,即把SimpleDateFormat的对象,在线程中生成对象,使用时,线程间使用不同的SimpleDateFormat对象,那么单个线程修改Calendar对象,不会影响其它线程的SimpleDateFormat对象。嗯,这个思路是正确的。代码如下:
private ThreadLocal<SimpleDateFormat> thDefault = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
@Test
public void testSimDateFormatByThreadLocalDefault() throws InterruptedException {
final Semaphore semaphore = new Semaphore(thdNum);
final CountDownLatch countDownLatch = new CountDownLatch(executeCount);
ExecutorService executor = Executors.newCachedThreadPool();
for(int i=0; i< executeCount; i++) {
executor.submit(() -> {
try {
semaphore.acquire();
try {
thDefault.get().parse("2024-01-01");
} catch (Exception e) {
System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
System.exit(-1);
}
semaphore.release();
} catch (InterruptedException e) {
System.out.println("信号量发生錯誤");
e.printStackTrace();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executor.shutdown();
System.out.println("所有线程格式化日期成功");
}
ThreadLocal对象thDefault,按线程来创建SimpleDateFormat对象,运行结果如下:
所有线程格式化日期成功
进程已结束,退出代码 0
这种方式是线程安全的,每个线程使用自己的SimpleDateFormat对象,线程之间互不影响,同时也不会产生大量的SimpleDateFormat对象,触发频发的gc。高并发场景下推荐使用。
线程独占SimpleDateFormat对象方式二
与线程独占SimpleDateFormat对象的方式类似,在写法上有所不同,可根据实际情况选择使用。代码如下:
private ThreadLocal<SimpleDateFormat> th = new ThreadLocal<>();
private SimpleDateFormat getSimpleDateFormat(){
SimpleDateFormat sdf = th.get();
if(sdf==null){
sdf = new SimpleDateFormat("yyyy-MM-dd");
th.set(sdf);
}
return sdf;
}
@Test
public void testSimDateFormatByThreadLocal() throws InterruptedException {
final Semaphore semaphore = new Semaphore(thdNum);
final CountDownLatch countDownLatch = new CountDownLatch(executeCount);
ExecutorService executor = Executors.newCachedThreadPool();
for(int i=0; i< executeCount; i++) {
executor.submit(() -> {
try {
semaphore.acquire();
try {
getSimpleDateFormat().parse("2024-01-01");
} catch (Exception e) {
System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
System.exit(-1);
}
semaphore.release();
} catch (InterruptedException e) {
System.out.println("信号量发生錯誤");
e.printStackTrace();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executor.shutdown();
System.out.println("所有线程格式化日期成功");
}
使用ThreadLocal线程对象th,通过getSimpleDateFormat()方法获取SimpleDateFormat对象,当本地线程对象中存在SimpleDateFormat时则直接使用,当不存在时,先创建SimpleDateFormat对象,放到本地线程变量中保存,然后再使用。推荐使用。运行结果如下:
所有线程格式化日期成功
进程已结束,退出代码 0
DateTimeFormatter处理日期
DateTimeFormatter是Java 8中引入的一个用于日期和时间格式化的实用类。它提供了一种简便的方法来格式化日期和时间,同时还可以将日期和时间字符串解析为Java对象。DateTimeFormatter提供了预定义的格式化模式,允许开发者根据需要自定义日期和时间的格式,并以字符串的形式输出。
DateTimeFormatter优点
线程安全
与早期的SimpleDateFormat类不同,DateTimeFormatter是线程安全的。这意味着在多线程应用程序中,可以无需担心同步问题而安全地使用DateTimeFormatter。因此,无需为每个线程创建独立的DateTimeFormatter实例,或者将其放入ThreadLocal中,简化了多线程环境下的日期和时间处理。
不可变性
DateTimeFormatter对象一旦创建,其状态是不可修改的。这种不可变性确保了DateTimeFormatter对象在多次使用时的稳定性和一致性,进一步增强了线程安全性。
支持自定义格式
DateTimeFormatter不仅提供了预定义的日期和时间格式,还支持自定义格式。开发者可以根据需要指定日期和时间的格式模式,以满足特定的显示要求。这使得DateTimeFormatter非常灵活,能够适应各种日期和时间格式化的场景。
本地化支持
DateTimeFormatter可以根据不同的语言和国家的习惯来格式化日期和时间。它支持多语言环境,能够自动根据当前的语言环境选择适当的日期和时间格式,使得应用程序更具国际化特性。
扩展性
DateTimeFormatter还提供了DateTimeFormatterBuilder类,用于创建自定义的日期时间格式化器。这使得开发者能够根据自己的需求,构建复杂的日期和时间格式化规则,实现更高级的格式化功能。
DateTimeFormatter使用
栗子
import java.time.format.DateTimeFormatter;
private static final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@Test
public void testDateTimeFormatter() throws InterruptedException{
final Semaphore semaphore = new Semaphore(thdNum);
final CountDownLatch countDownLatch = new CountDownLatch(executeCount);
ExecutorService executor = Executors.newCachedThreadPool();
for(int i=0; i< executeCount; i++) {
executor.submit(() -> {
try {
semaphore.acquire();
try {
dtf.parse("2024-01-01");
} catch (Exception e) {
System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
e.printStackTrace();
System.exit(-1);
}
semaphore.release();
} catch (InterruptedException e) {
System.out.println("信号量发生錯誤");
e.printStackTrace();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executor.shutdown();
System.out.println("所有线程格式化日期成功");
}
定义DateTimeFormatter对象为全局唯一对象,使用方式与常规SimpleDateFormat类似。强烈推荐使用。
结果
所有线程格式化日期成功
进程已结束,退出代码 0
JotTime处理日期
JotTime是一款功能强大的时间管理应用,旨在帮助用户更有效地管理时间和任务。这款应用结合了简洁直观的界面和多种实用的功能,使用户能够轻松地跟踪、组织和优化他们的日程安排。
JotTime优点
直观易用的界面
JotTime的界面设计简洁直观,使用户能够快速上手并熟悉各项功能。无论是添加任务、设置提醒还是查看日程,操作都非常流畅便捷,无需花费大量时间学习。
强大的任务管理功能
JotTime提供了丰富的任务管理功能,包括任务创建、编辑、删除、优先级设置等。用户可以根据需要轻松组织任务,设置截止日期和提醒,确保工作有条不紊地进行。
精准的时间跟踪
JotTime能够精确地跟踪每项任务的耗时情况,帮助用户了解自己的工作效率和习惯。通过收集和分析时间数据,用户可以找出自己的时间管理瓶颈,并进行相应的优化。
灵活的提醒与通知
JotTime支持多种提醒方式和自定义提醒时间,确保用户不会错过任何重要任务。无论是弹窗提醒、震动提醒还是声音提醒,用户都可以根据自己的需求进行设置,提高任务的按时完成率。
强大的同步与协作能力
JotTime支持与多种日历应用同步,使得用户可以在不同平台之间无缝协作。同时,JotTime还支持共享任务和日历,方便团队成员之间的沟通和协作,提高团队效率。
个性化定制与扩展性
JotTime允许用户根据个人喜好定制界面和提醒方式,满足不同用户的需求。此外,JotTime还提供了丰富的扩展功能,如报告与统计、数据导入导出等,进一步提升了其应用价值。
JotTime使用
Maven引入
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.9.9</version>
</dependency>
栗子
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.DateTime;
private static final DateTimeFormatter dtf = DateTimeFormat.forPattern("yyyy-MM-dd");
@Test
public void testJotTime() throws InterruptedException {
final Semaphore semaphore = new Semaphore(thdNum);
final CountDownLatch countDownLatch = new CountDownLatch(executeCount);
ExecutorService executor = Executors.newCachedThreadPool();
for(int i=0; i< executeCount; i++) {
executor.submit(() -> {
try {
semaphore.acquire();
try {
DateTime.parse("2024-01-01", dtf).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();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executor.shutdown();
System.out.println("所有线程格式化日期成功");
}
JotTime的使用与SimpleDateFormat有多不同,需要声明DateTimeFormatter对象,通过DateTimeFormat.forPattern()实例化,通过DateTime.parse(“日期”, dtf).toDate()调用。
结果
所有线程格式化日期成功
进程已结束,退出代码 0
总结
SimpleDateFormat是常用的日期处理工具,在日常开发中经常会用到,但SimpleDateFormat为线程不安全的,在高并发场景下,如果一定要使用SimpleDateFormat,需考虑线程安全问题,同时需最大限度的减少SimpleDateFormat对象的频发创建,减少gc的负担,可以通过在每个线程中保存SimpleDateFormat对象变量的方式使用。日期处理工具除了SimpleDateFormat外,还有DateTimeFormatter和JotTime,这两个日期处理工具均为线程安全的,同时具有高性能等优势,推荐在高并发场景中使用。
原文始发于微信公众号(扬哥手记):高并发这样用SimpleDateFormat,快醒醒
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/261597.html