日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

Java Review - SimpleDateFormat线程不安全原因的源码分析及解决办法

發(fā)布時間:2025/3/21 java 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java Review - SimpleDateFormat线程不安全原因的源码分析及解决办法 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

文章目錄

  • 概述
  • 復現(xiàn)問題
  • 源碼分析
  • How to Fix ?
    • 每次使用時new一個SimpleDateFormat的實例
    • 加鎖
    • 使用ThreadLocal
    • 換API - JodaTime or JDK1.8的時間類
  • 小結(jié)


概述

SimpleDateFormat是Java提供的一個格式化和解析日期的工具類,在日常開發(fā)中經(jīng)常會用到,但是由于它是線程不安全的,所以多線程共用一個SimpleDateFormat實例對日期進行解析或者格式化會導致程序出錯。

這里來揭示它為何是線程不安全的,以及如何避免該問題。


復現(xiàn)問題

import java.text.ParseException; import java.text.SimpleDateFormat; /*** @author 小工匠* @version 1.0* @description: TODO* @date 2021/11/21 14:56* @mark: show me the code , change the world*/ public class SimpleDateFormatTest {// 1 創(chuàng)建單例實例private static SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss");public static void main(String[] args) {// 2 開啟多個線程,并且歐東for (int i = 0; i < 10; i++) {Thread thread = new Thread(() -> {try {// 3 使用單例日期解析文本System.out.println(sdf.parse("2021-11-19 15:15:00"));} catch (ParseException e) {e.printStackTrace();}});thread.start();}} }

代碼(1)創(chuàng)建了SimpleDateFormat的一個實例

代碼(2)創(chuàng)建10個線程,每個線程都共用同一個sdf對象對文本日期進行解析。

多運行幾次代碼就會拋出java.lang.NumberFormatException異常,增加線程的個數(shù)有利于復現(xiàn)該問題

Exception in thread "Thread-0" Exception in thread "Thread-2" Exception in thread "Thread-1" Exception in thread "Thread-6" Exception in thread "Thread-4" Exception in thread "Thread-8" Exception in thread "Thread-9" Exception in thread "Thread-5" Exception in thread "Thread-7" 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:2084)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.artisan.bfzm.chapter11.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:26)at java.lang.Thread.run(Thread.java:748) java.lang.ArrayIndexOutOfBoundsException: 20at java.text.DigitList.append(DigitList.java:151)at java.text.DecimalFormat.subparse(DecimalFormat.java:2278)at java.text.DecimalFormat.parse(DecimalFormat.java:2036)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.artisan.bfzm.chapter11.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:26)at java.lang.Thread.run(Thread.java:748) 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:2089)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.artisan.bfzm.chapter11.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:26)at java.lang.Thread.run(Thread.java:748) 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:2084)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.artisan.bfzm.chapter11.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:26)at java.lang.Thread.run(Thread.java:748) 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:2089)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.artisan.bfzm.chapter11.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:26)at java.lang.Thread.run(Thread.java:748) 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:2089)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.artisan.bfzm.chapter11.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:26)at java.lang.Thread.run(Thread.java:748) java.lang.ArrayIndexOutOfBoundsException: 19at java.text.DigitList.append(DigitList.java:151)at java.text.DecimalFormat.subparse(DecimalFormat.java:2278)at java.text.DecimalFormat.parse(DecimalFormat.java:2036)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.artisan.bfzm.chapter11.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:26)at java.lang.Thread.run(Thread.java:748) 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:2089)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 .....


源碼分析

為了便于分析,首先來看SimpleDateFormat的類圖結(jié)構(gòu)

可以看到,每個SimpleDateFormat實例里面都有一個Calendar對象,后面我們就會知道,SimpleDateFormat之所以是線程不安全的,就是因為Calendar是線程不安全的。

Calendar之所以是線程不安全的,是因為其中存放日期數(shù)據(jù)的變量都是線程不安全的,比如fields、time等。

下面從代碼層面來看下parse方法做了什么事情。

public Date parse(String source) throws ParseException{ParsePosition pos = new ParsePosition(0);Date result = parse(source, pos);if (pos.index == 0)throw new ParseException("Unparseable date: \"" + source + "\"" ,pos.errorIndex);return result;}@Overridepublic Date parse(String text, ParsePosition pos){.......// 1 解析日期字符串,將解析好的數(shù)據(jù)放入CalendarBuilder對象中CalendarBuilder calb = new CalendarBuilder();..............Date parsedDate;try {// 2 使用calb中解析好的日期數(shù)據(jù)設(shè)置calendarparsedDate = calb.establish(calendar).getTime();} catch (IllegalArgumentException e) {..............return null;}..............return parsedDate;}
  • 代碼(1)的主要作用是解析日期字符串并把解析好的數(shù)據(jù)放入 CalendarBuilder的實例calb中。CalendarBuilder是一個建造者模式,用來存放后面需要的數(shù)據(jù)。

  • 代碼(2)使用calb中解析好的日期數(shù)據(jù)設(shè)置calendar。

calb.establish的代碼如下

Calendar establish(Calendar cal) {.....// 3 重置日期對象cal的屬性值cal.clear();// 4 使用calb中的屬性設(shè)置cal// 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);}// 5 返回calreturn cal;}
  • 代碼(3)重置Calendar對象里面的屬性值,如下所示。
public final void clear(){for (int i = 0; i < fields.length; ) {stamp[i] = fields[i] = 0; // UNSET == 0isSet[i++] = false;}areAllFieldsSet = areFieldsSet = false;isTimeSet = false;}
  • 代碼(4)使用calb中解析好的日期數(shù)據(jù)設(shè)置cal對象。

  • 代碼(5) 返回設(shè)置好的cal對象。

從以上代碼可以看出,代碼(3)、代碼(4)和代碼(5)并不是原子性操作。

當多個線程調(diào)用parse 方法時,比如線程A執(zhí)行了代碼(3)和代碼(4),也就是設(shè)置好了cal對象,但是在執(zhí)行代碼(5)之前,線程B執(zhí)行了代碼(3),清空了cal對象。

由于多個線程使用的是一個cal對象,所以線程A執(zhí)行代碼(5)返回的可能就是被線程B清空的對象,當然也有可能線程B執(zhí)行了代碼(4),設(shè)置被線程A修改的cal對象,從而導致程序出現(xiàn)錯誤。


How to Fix ?

每次使用時new一個SimpleDateFormat的實例

每次使用時new一個SimpleDateFormat的實例,這樣可以保證每個實例使用自己的Calendar實例,但是每次使用都需要new一個對象,并且使用后由于沒有其他引用,又需要回收,開銷會很大。


加鎖

出錯的根本原因是因為多線程下代碼(3)、代碼(4)和代碼(5)三個步驟不是一個原子性操作,那么容易想到的是對它們進行同步,讓代碼(3)、代碼(4)和代碼(5)成為原子性操作。

可以使用synchronized進行同步,具體如下。

進行同步意味著多個線程要競爭鎖,在高并發(fā)場景下這會導致系統(tǒng)響應(yīng)性能下降。


使用ThreadLocal

使用ThreadLocal,這樣每個線程只需要使用一個SimpleDateFormat實例,這相比第一種方式大大節(jié)省了對象的創(chuàng)建銷毀開銷,并且不需要使多個線程同步。

使用ThreadLocal方式的代碼如下

import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat;/*** @author 小工匠* @version 1.0* @description: TODO* @date 2021/11/21 14:56* @mark: show me the code , change the world*/ public class SimpleDateFormatTest {// 1 創(chuàng)建ThreadLocal實例static ThreadLocal<DateFormat> threadLocal= ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));// static ThreadLocal<DateFormat> threadLocal2 = new ThreadLocal<DateFormat>(){ // @Override // protected DateFormat initialValue() { // return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // } // };public static void main(String[] args) {// 2 開啟多個線程,并且歐東for (int i = 0; i < 10; i++) {Thread thread = new Thread(() -> {try {// 3 使用單例日期解析文本System.out.println(threadLocal.get().parse("2021-11-19 15:15:00"));} catch (ParseException e) {e.printStackTrace();}finally {// 4 使用完畢,一定要removethreadLocal.remove();}});thread.start();}} }
  • 代碼(1)創(chuàng)建了一個線程安全的SimpleDateFormat實例

  • 代碼(3)首先使用get()方法獲取當前線程下SimpleDateFormat的實例。在第一次調(diào)用ThreadLocal的get()方法時,會觸發(fā)其initialValue方法創(chuàng)建當前線程所需要的SimpleDateFormat對象。

  • 另外需要注意的是,在代碼(4)中,使用完線程變量后,要進行清理,以避免內(nèi)存泄漏。


換API - JodaTime or JDK1.8的時間類

Java 8的日期和時間類包含LocalDate、LocalTime、Instant、Duration以及Period,這些類都包含在java.time包中.

新的日期API中提供了一個DateTimeFormatter類用于處理日期格式化操作,它被包含在java.time.format包中,Java 8的日期類有一個format()方法用于將日期格式化為字符串,該方法接收一個DateTimeFormatter類型參數(shù).

public static void main(String[] args) {// 2 開啟多個線程,for (int i = 0; i < 10; i++) {Thread thread = new Thread(() -> {// 3 使用單例日期解析文本System.out.println(LocalDateTime.parse("2021-11-19 15:15:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));});thread.start();}}

小結(jié)

我們這里簡單介紹SimpleDateFormat的原理解釋了為何SimpleDateFormat是線程不安全的,應(yīng)該避免在多線程下使用SimpleDateFormat的單個實例

總結(jié)

以上是生活随笔為你收集整理的Java Review - SimpleDateFormat线程不安全原因的源码分析及解决办法的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。