建站之星网站 和服务器,微信开放平台开发者,湖南网站推广公司,教外国人做中国菜网站目录
情景重现
SimpleDateFormat解析
解决方案
局部变量 加锁 使用线程变量
使用DateTimeFormatter 情景重现
SimpleDateFormat类是Java开发中的一个日期时间的转化类。它可以满足绝大多数的开发场景#xff0c;但是在高并发下会出现并发问题。接下来查看下文中的案例。…目录
情景重现
SimpleDateFormat解析
解决方案
局部变量 加锁 使用线程变量
使用DateTimeFormatter 情景重现
SimpleDateFormat类是Java开发中的一个日期时间的转化类。它可以满足绝大多数的开发场景但是在高并发下会出现并发问题。接下来查看下文中的案例。
public class TestSimpleDateFormat {public static void main(String[] args) {SimpleDateFormat format new SimpleDateFormat(yyyy-MM-dd);for (int i 0; i 5; i) {new Thread(()-{try {Date parse format.parse(2003-01-01);System.out.println(parse);} catch (Exception e) {e.printStackTrace();}}).start();}}
}
上面代码简单来说就是创建了一个SimpleDateFormat类对象该对象被后续会被五个线程使用去转化日期格式并打印。我们来查看输出结果。
java.lang.NumberFormatException: empty Stringat 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:1869)at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)at java.text.DateFormat.parse(DateFormat.java:364)at test.lambda$main$0(test.java:21)at java.lang.Thread.run(Thread.java:745)
java.lang.NumberFormatException: multiple pointsat 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 test.lambda$main$0(test.java:21)at java.lang.Thread.run(Thread.java:745)
java.lang.NumberFormatException: multiple pointsat 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 test.lambda$main$0(test.java:21)at java.lang.Thread.run(Thread.java:745)
java.lang.NumberFormatException: empty Stringat 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:1869)at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)at java.text.DateFormat.parse(DateFormat.java:364)at test.lambda$main$0(test.java:21)at java.lang.Thread.run(Thread.java:745)
Fri Nov 01 00:00:00 CST 2222可以看到只输出了一次时间转化并且该输出格式还是错误的。接下来我们来查看为什么SimpleDateFormat类是线程不安全的。
SimpleDateFormat解析
我们根据parse()方法查看SimpleDateFormat是如何进行格式转换的。 我们可以看到返回结果是根据另一个方法获取到的接下来我们接着查看该parse()源码。 这是一个抽象方法接着我们去查看它的具体实现。 可以看到该方法很长但是我们只关注返回如何结果直接拉到最后查看该方法如何返回一个日期格式 。上图中最后一次修改parseDate对象是在箭头的位置。那么我们查看getTime()方法。 可以看到该方法是由Calendar类提供的该类名翻译为中文就是日历的意思并且返回结果也是我们需要的日期格式那么我们就可以确定该方法用于给parseDate对象提供返回值的接下来回退一下查看其他方法哪个是提供Calendar对象来调用getTime()方法的。 现在我们清楚了Calendar类对象是由establish()方法提供的了该方法中需要一个参数calendar对象。该对象由SimpleDateFormat的父类DateFormat来维护。 此时我们或许大概明白是因为SimpleDateFormat类之所以线程不安全的问题是因为在多线程下共享了calendar对象。接下来我们继续查看establish()方法验证是否是这样下面是具体源码 Calendar establish(Calendar cal) {boolean weekDate isSet(WEEK_YEAR) field[WEEK_YEAR] field[YEAR];if (weekDate !cal.isWeekDateSupported()) {// Use YEAR insteadif (!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对象执行了clear()方法与set()方法我们查看clear方法是做什么的 该方法提供了类似初始化的功能将上一次的格式转化保存的cal属性清除。 而set()方法将本次的格式转换需要的数据更新。因此我们可以确定了SimpleDateFormat类之所以线程不安全就是因为共享了calendar对象。
解决方案
为了避免SimpleDateFormat格式转换带来的并发问题我们可以采取以下几个措施
局部变量
我们已经知道了产生线程安全问题的原因是共享了相同属性那么我们只要让每个线程都包含自己的属性就可以避免该问题的发生。具体实现代码如下
public class TestSimpleDateFormat {public static void main(String[] args) {for (int i 0; i 5; i) {new Thread(()-{try {SimpleDateFormat format new SimpleDateFormat(yyyy-MM-dd);Date parse format.parse(2003-01-01);System.out.println(parse);} catch (Exception e) {e.printStackTrace();}}).start();}}
} 运行结果如下 Wed Jan 01 00:00:00 CST 2003
Wed Jan 01 00:00:00 CST 2003
Wed Jan 01 00:00:00 CST 2003
Wed Jan 01 00:00:00 CST 2003
Wed Jan 01 00:00:00 CST 2003
这种方式不太推荐因为会创建大量的SimpleDateFormat对象占用内存空间。 加锁
除了让每个线程都拥有自己独立的对象外我们也可以保证在同一时刻下只有一个线程对共享属性进行修改那就是加锁。具体实现代码如下
public class TestSimpleDateFormat{public static void main(String[] args) {SimpleDateFormat format new SimpleDateFormat(yyyy-MM-dd);for (int i 0; i 5; i) {new Thread(() - {try {Date parse;synchronized (format){parse format.parse(2003-01-01);}System.out.println(parse);} catch (Exception e) {e.printStackTrace();}}).start();}}
}
但是这种方法不太推荐因为能够出现格式转化错误的情况已经是很大的并发了如果还使用同步锁的话会影响性能。 使用线程变量
public class test {private static ThreadLocalDateFormat threadLocal new ThreadLocalDateFormat() {Overrideprotected DateFormat initialValue() {return new SimpleDateFormat(yyyy-MM-dd);}};public static void main(String[] args) {for (int i 0; i 5; i) {new Thread(() - {try {Date parse threadLocal.get().parse(2023-01-01);System.out.println(parse);} catch (ParseException e) {e.printStackTrace();}}).start();}}
}
使用DateTimeFormatter
在JDK8之后提供了线程安全的格式转化DateTimeFormatter类使用方法如下
public class test {public static void main(String[] args) {DateTimeFormatter dateTimeFormatter DateTimeFormatter.ofPattern(yyyy-MM-dd);for (int i 0; i 5; i) {new Thread(() - {try {TemporalAccessor parse dateTimeFormatter.parse(2003-06-03);System.out.println(parse);} catch (Exception e) {e.printStackTrace();}}).start();}}
} 输出结果为 {},ISO resolved to 2003-06-03
{},ISO resolved to 2003-06-03
{},ISO resolved to 2003-06-03
{},ISO resolved to 2003-06-03
{},ISO resolved to 2003-06-03