91欧美超碰AV自拍|国产成年人性爱视频免费看|亚洲 日韩 欧美一厂二区入|人人看人人爽人人操aV|丝袜美腿视频一区二区在线看|人人操人人爽人人爱|婷婷五月天超碰|97色色欧美亚州A√|另类A√无码精品一级av|欧美特级日韩特级

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

糟糕,被SimpleDateFormat坑到啦!

京東云 ? 來源:jf_75140285 ? 作者:jf_75140285 ? 2024-09-06 09:45 ? 次閱讀
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

1. 問題背景

問題的背景是這樣的,在最近需求開發(fā)中遇到需要將給定目標數據通過某一固定的計量規(guī)則進行過濾并打標生成明細數據,其中發(fā)現存在一筆目標數據的時間在不符合現有日期規(guī)則的條件下,還是通過了規(guī)則引擎的匹配打標操作。故而需要對該錯誤匹配場景進行排查,定位根本原因所在。

2. 排查思路

2.1 數據定位

在開始排查問題之初,先假定現有的Aviator規(guī)則引擎能夠對現有的數據進行正常的匹配打標,查詢在存在問題數據(圖中紅框所示)同一時刻進行規(guī)則匹配時的數據都有哪些。發(fā)現存在五筆數據在同一時刻進行規(guī)則匹配落庫。

wKgaombaXpCANXfmAAGe5bR-pqY435.png

繼續(xù)查詢具體的匹配規(guī)則表達式,發(fā)現針對loanPayTime時間區(qū)間在[2022-07-16 00:00:00, 2023-05-11 23:59:59]的范圍內進行匹配,目標數據的時間為2023-09-19 11:27:29,理論上應該不會被匹配到。

wKgZombaXpGAe-HlAABHT4Fj8qw463.png

但是觀測匹配打標的明細數據發(fā)現確實打標成功了(如紅框所示)。

wKgaombaXpKABZ6IAAGh1Bn0M7E195.png

所以重新回到最初的和目標數據同時落庫的五筆數據發(fā)現,這五筆數據的loanPayTime時間確實在規(guī)則[2022-07-16 00:00:00, 2023-05-11 23:59:59]之內,所以在想有沒有可能是在目標數據匹配規(guī)則引擎,其它的五筆數據中的其中一筆對該數據進行了修改導致誤匹配到了這個規(guī)則。順著這個思路,首先需要確認下Aviator規(guī)則引擎在并發(fā)場景下是否線程安全的。

wKgZombaXpaAPKazAAG9HT1yXuU440.png

2.2 規(guī)則引擎

由于在需求中使用到用于給數據匹配打標的是Aviator規(guī)則引擎,所以第一直覺是懷疑Aviator規(guī)則引擎在并發(fā)的場景中可能會存在線程不安全的情況。

wKgaombaXpeAFAwrAACPrAnrQf0551.png

首先簡單介紹下Aviator規(guī)則引擎是什么,Aviator是一個高性能的、輕量級java語言實現的表達式求值引擎,主要用于各種表達式的動態(tài)求值,相較于其它的開源可用的規(guī)則引擎而言,Aviator的設計目標是輕量級和高性能 ,相比于Groovy、JRuby的笨重,Aviator非常小,加上依賴包也才450K,不算依賴包的話只有70K;

當然,Aviator的語法是受限的,它不是一門完整的語言,而只是語言的一小部分集合。其次,Aviator的實現思路與其他輕量級的求值器很不相同,其他求值器一般都是通過解釋的方式運行,而Aviator則是直接將表達式編譯成Java字節(jié)碼,交給JVM去執(zhí)行。簡單來說,Aviator的定位是介于Groovy這樣的重量級腳本語言和IKExpression這樣的輕量級表達式引擎之間。(具體Aviator的相關介紹不是本文的重點,具體可參見)

通過查閱相關資料發(fā)現,Aviator中的AviatorEvaluator.execute() 方法本身是線程安全的,也就是說只要表達式執(zhí)行邏輯和傳入的env是線程安全的,理論上是不會出現并發(fā)場景下線程不安全問題的。(詳見)

2.3 匹配規(guī)則引擎的env

wKgZombaXpiAbcE5AABTqU--LnI055.png

通過前面Aviator的相關資料發(fā)現傳入的env如果在多線程場景下不安全也會導致最終的結果是錯誤的,故而定位使用的env發(fā)現使用的是HashMap,該集合類確實是線程不安全的(具體可詳見),但是線程不安全的前提是多個線程同時對其進行修改,定位代碼發(fā)現在每次調用方式時都會重新生成一個HashMap,故而應該不會是由于這個線程不安全類導致的。

wKgaombaXpmABWGSAADNnSDuIgY560.png

繼續(xù)定位發(fā)現,loanPayTime這個字段在進行Aviator規(guī)則引擎匹配前使用SimpleDateFormat進行了格式化,所以有可能是由于該類的線程不安全導致的數據錯亂問題,但是這個類應該只是對日期進行格式化處理,難不成還能影響最終的數據。帶著這個疑問查詢資料發(fā)現,emm確實是線程不安全的。

wKgZombaXpqAJhg_AAKEKTD57Ws016.png

好家伙,嫌疑對象目前已經有了,現在就是尋找相關證據來佐證了。

3. SimpleDateFormat 還能線程不安全?

3.1 先寫個demo試試

話不多說,直接去測試一下在并發(fā)場景下,SimpleDateFormat類會不會對需要格式化的日期進行錯亂格式化。先模擬一個場景,對多線程并發(fā)場景下格式化日期,即在[0,9]的數據范圍內,在偶數情況下對2024年1月23日進行格式化,在奇數情況下對2024年1月22日進行格式化,然后觀測日志打印效果。

import java.text.SimpleDateFormat;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ThreadSafeDateFormatDemo {
    static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");


    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(10);
        LocalDateTime startDateTime = LocalDateTime.now();
        Date date = new Date();
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            executor.submit(() -?> {
                try {
                    if (finalI % 2 == 0) {

                        String formattedDate = dateFormat.format(date);
                        //第一種
//                        String formattedDate = DateUtil.formatDate(date);
                        //第二種
//                        String formattedDate = DateSyncUtil.formatDate(date);
                        //第三種
//                        String formattedDate = ThreadLocalDateUtil.formatDate(date);
                        System.out.println("線程 " + Thread.currentThread().getName() + " 時間為: " + formattedDate + " 偶數i:" + finalI);
                    } else {
                        Date now = new Date();
                        now.setTime(now.getTime() - TimeUnit.MILLISECONDS.convert(1, TimeUnit.DAYS));
                        String formattedDate = dateFormat.format(now);
                        //第一種
//                        String formattedDate = DateUtil.formatDate(now);
                        //第二種
//                        String formattedDate = DateSyncUtil.formatDate(now);
                        //第三種
//                        String formattedDate = ThreadLocalDateUtil.formatDate(now);
                        System.out.println("線程 " + Thread.currentThread().getName() + " 時間為: " + formattedDate + " 奇數i:" + finalI);
                    }

                } catch (Exception e) {
                    System.err.println("線程 " + Thread.currentThread().getName() + " 出現了異常: " + e.getMessage());
                }
            });
        }

        executor.shutdown();
        try {
            executor.awaitTermination(30, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 計算總耗時
        LocalDateTime endDateTime = LocalDateTime.now();
        Duration duration = Duration.between(startDateTime, endDateTime);
        System.out.println("所有任務執(zhí)行完畢,總耗時: " + duration.toMillis() + " 毫秒");
    }
}

具體demo代碼如上所示,執(zhí)行結果如下,理論上來說應該是2024年1月23日2024年1月22日打印日志的次數各5次。實際結果發(fā)現在偶數的場景下仍然會出現打印格式化2024年1月22日的場景。明顯出現了數據錯亂賦值的問題,所以到這里大概可以基本確定就是SimpleDateFormat類在并發(fā)場景下線程不安全導致的

wKgZombaXp2Acjb2AAEwnhsTG2I866.png

3.2 SimpleDateFormat為什么線程不安全?

查詢相關資料發(fā)現,從SimpleDateFormat類提供的接口來看,實在讓人看不出它與線程安全有什么關系,進入SimpleDateFormat源碼發(fā)現類上面確實存在注釋提醒:意思就是, SimpleDateFormat中的日期格式不是同步的。推薦(建議)為每個線程創(chuàng)建獨立的格式實例。如果多個線程同時訪問一個格式,則它必須保持外部同步。

wKgaombaXp6ARHuCAAGXORfBsp0840.png

繼續(xù)分析源碼發(fā)現,SimpleDateFormat線程不安全的真正原因是繼承了DateFormat,DateFormat中定義了一個protected屬性的 Calendar類的對象:calendar。由于Calendar類的概念復雜,牽扯到時區(qū)與本地化等等,jdk的實現中使用了成員變量來傳遞參數,這就造成在多線程的時候會出現錯誤。

wKgZombaXqCALSSGAAGEsqLatYw884.png

注意到在format方法中有一段如下代碼:

 public StringBuffer format(Date date, StringBuffer toAppendTo,
                               FieldPosition pos)
    {
        pos.beginIndex = pos.endIndex = 0;
        return format(date, toAppendTo, pos.getFieldDelegate());
    }

    // Called from Format after creating a FieldDelegate
    private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {
        // Convert input date to time field list
        calendar.setTime(date);

        boolean useDateFormatSymbols = useDateFormatSymbols();

        for (int i = 0; i < compiledPattern.length; ) {
            int tag = compiledPattern[i] >>> 8;
            int count = compiledPattern[i++] & 0xff;
            if (count == 255) {
                count = compiledPattern[i++] 

calendar.setTime(date)這條語句改變了calendar,稍后,calendar還會用到(在subFormat方法里),而這就是引發(fā)問題的根源。

想象一下,在一個多線程環(huán)境下,有兩個線程持有了同一個SimpleDateFormat的實例,分別調用format方法: 線程1調用format方法,改變了calendar這個字段。 中斷來了。 線程2開始執(zhí)行,它也改變了calendar。 又中斷了。 線程1回來了,此時,calendar已然不是它所設的值,而是走上了線程2設計的道路。

如果多個線程同時爭搶calendar對象,則會出現各種問題,時間不對,線程掛死等等。 分析一下format的實現,我們不難發(fā)現,用到成員變量calendar,唯一的好處,就是在調用subFormat時,少了一個參數,卻帶來了這許多的問題。

其實,只要在這里用一個局部變量,一路傳遞下去,所有問題都將迎刃而解。 這個問題背后隱藏著一個更為重要的問題–無狀態(tài):無狀態(tài)方法的好處之一,就是它在各種環(huán)境下,都可以安全的調用。衡量一個方法是否是有狀態(tài)的,就看它是否改動了其它的東西,比如全局變量,比如實例的字段。format方法在運行過程中改動了SimpleDateFormat的calendar字段,所以,它是有狀態(tài)的。

4. 如何解決?

4.1 每次在需要時新創(chuàng)建實例

在需要進行格式化日期的地方新建一個實例,不管什么時候,將有線程安全問題的對象由共享變?yōu)榫植克接卸寄鼙苊舛嗑€程問題,不過也加重了創(chuàng)建對象的負擔。在一般情況下,這樣其實對性能影響比不是很明顯的。代碼示例如下。

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author 
 * @date 2024/1/23 20:04
 */


public class DateUtil {

    public static String formatDate(Date date) throws ParseException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.format(date);
    }

    public static Date parse(String strDate) throws ParseException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.parse(strDate);
    }
}?

4.2 同步SimpleDateFormat對象

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author 
 * @date 2024/1/23 20:04
 */


public class DateSyncUtil {

    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static String formatDate(Date date) throws ParseException {
        synchronized (sdf) {
            return sdf.format(date);
        }
    }

    public static Date parse(String strDate) throws ParseException {
        synchronized (sdf) {
            return sdf.parse(strDate);
        }
    }
}

說明:當線程較多時,當一個線程調用該方法時,其他想要調用此方法的線程就要block,多線程并發(fā)量大的時候會對性能有一定的影響。

4.3 ThreadLocal

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class ConcurrentDateUtil {

    private static ThreadLocal threadLocal = new ThreadLocal() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static Date parse(String dateStr) throws ParseException {
        return threadLocal.get().parse(dateStr);
    }

    public static String format(Date date) {
        return threadLocal.get().format(date);
    }
}

另一種寫法

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author 
 * @date 2024/1/23 15:44
 * @description 線程安全的日期處理類
 */


public class ThreadLocalDateUtil {
    /**
     * 日期格式
     */
    private static final String date_format = "yyyy-MM-dd HH:mm:ss";
    /**
     * 線程安全處理
     */
    private static ThreadLocal threadLocal = new ThreadLocal();

    /**
     * 線程安全處理
     */
    public static DateFormat getDateFormat() {
        DateFormat df = threadLocal.get();
        if (df == null) {
            df = new SimpleDateFormat(date_format);
            threadLocal.set(df);
        }
        return df;
    }

    /**
     * 線程安全處理日期格式化
     */
    public static String formatDate(Date date) {
        return getDateFormat().format(date);
    }

    /**
     * 線程安全處理日期解析
     */
    public static Date parse(String strDate) throws ParseException {
        return getDateFormat().parse(strDate);
    }
}

說明:使用ThreadLocal, 也是將共享變量變?yōu)楠毾?,線程獨享肯定能比方法獨享在并發(fā)環(huán)境中能減少不少創(chuàng)建對象的開銷。如果對性能要求比較高的情況下,一般推薦使用這種方法

4.4 拋棄JDK,使用其他類庫中的時間格式化類

?使用

Apache commons

里的

FastDateFormat

,宣稱是既快又線程安全的SimpleDateFormat, 可惜它

只能

對日期進行format,

不能

對日期串進行解析。?使用

Joda-Time

類庫來處理時間相關問題。

5. 性能比較

通過追加時間監(jiān)控,將原有數據范圍擴充到[0,999],線程池保留10個線程不變,觀察三種情況下性能情況。

?第一種:耗時40ms

wKgaombaXqCAH1PyAAIHquMz2Lk923.png

?第二種:耗時33ms

wKgZombaXqGAEfNnAAH-Naeabi0246.png

?第三種:耗時30ms

wKgaombaXqOAexakAAIU_W9WkC4323.png

通過性能壓測發(fā)現4.3中的ThreadLocal性能最優(yōu),耗時30ms,4.1每次新創(chuàng)建實例性能最差,需要耗時40ms,當然了在極致的高并發(fā)場景下提升效果應該會更加明顯。性能問題不是本文探討的重點,在此不多做贅述。

6. 總結

Ok,以上就是針對本次問題排查的主要思路及流程,這個我剛開始的排查思路也一直局限于規(guī)則引擎的線程不安全或者是傳入的env(由于使用的是HashMap)線程不安全,還是受到組內大佬的啟發(fā)和幫助才進一步去分析SimpleDateFormat類可能會存在線程不安全。本次問題排查確實提供一個經驗,打破常規(guī)思路,比如SimpleDateFormat類看起來只是對日期進行格式化,很難和在并發(fā)場景下線程不安全會導致數據錯亂關聯(lián)起來。以上。

審核編輯 黃宇

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • JAVA
    +關注

    關注

    20

    文章

    3002

    瀏覽量

    116460
  • hashmap
    +關注

    關注

    0

    文章

    15

    瀏覽量

    2527
收藏 人收藏
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

    評論

    相關推薦
    熱點推薦

    老板了,,,

    今天去廣州賽格那買一些元件。。如電阻啊,電容啊,三極管。。。第一次買這些東西,那的老板狂吃,卻不知道。。哎,買回來上網一看那些價格。。。果然是交學費了。。。。。
    發(fā)表于 11-18 13:10

    方波上下邊沿均出現糟糕的抖動

    光電編碼器輸出方波上下邊沿均出現糟糕的抖動?會不會是電機影響?求解?謝謝!
    發(fā)表于 04-22 20:57

    賺經驗積分

    一分錢難倒英雄好漢這不是嗎沒積分下載資料求支招~~~~
    發(fā)表于 07-08 15:03

    使用閃存最糟糕的環(huán)境是什么?

    使用閃存最糟糕的環(huán)境是什么? 以上來自于百度翻譯 以下為原文What kind of environment is the worst in using flash memory?
    發(fā)表于 12-07 14:39

    爛代碼你能忍嗎?優(yōu)秀的代碼VS糟糕的代碼

    糟糕的代碼原來那么不堪一擊。
    的頭像 發(fā)表于 03-30 10:09 ?4897次閱讀
    爛代碼你能忍嗎?優(yōu)秀的代碼VS<b class='flag-5'>糟糕</b>的代碼

    小米華為又蘋果,LG才是最大的“親王”?

    過硬的產品形象,出現多個問題產品。比如,華為手機Mate20 Pro就被LG的不淺,而蘋果似乎也馬上要成為下一個的對象。那么這到底是怎么回事,為何LG才是最大的“親王”? 本文
    的頭像 發(fā)表于 11-17 11:34 ?1027次閱讀

    怎樣對待水平糟糕的程序員

    這些年遇到了很多糟糕的程序員,其實真正是寫程序料的人,普通IT公司大概只占1/3左右吧,其實有2/3的人都太適合當程序員,還不如早點兒改行該干啥就干啥了,其中有1/10的人往往是相對比較糟糕的。
    的頭像 發(fā)表于 12-28 14:52 ?1815次閱讀

    網購鼠標和鍵盤時這幾個點最容易

    本篇給大家?guī)淼氖墙衲?15電商廉價的爹電競鍵鼠篇。很多學生黨或者預算很低但又喜愛玩電競游戲的朋友在某寶購買鍵盤和鼠標時,最容易掉進以下兩種鍵鼠的內。
    的頭像 發(fā)表于 03-15 10:36 ?1w次閱讀

    購買組裝電腦和配件有哪些方法避免

    越來越多的電腦小白入被騙,繼而維權困難,只能是啞巴吃黃連有苦說不出。其實網購有決竅,掌握以下幾點我們就能避免。
    的頭像 發(fā)表于 12-07 11:23 ?8104次閱讀

    使用Redis時可能遇到哪些「」?

    這篇文章,我想和你聊一聊在使用 Redis 時,可能會踩到的「」。 如果你在使用 Redis 時,也遇到過以下這些「詭異」的場景,那很大概率是踩到「」了: 明明一個 key 設置了過期時間
    的頭像 發(fā)表于 04-09 11:19 ?2980次閱讀
    使用Redis時可能遇到哪些「<b class='flag-5'>坑</b>」?

    嵌入式Linux踩記錄

    Linux踩記錄記錄Linux學習過程踩過的與如何解決踩1解決方法:F10進入BIOS使能虛擬化技術
    發(fā)表于 11-01 17:21 ?10次下載
    嵌入式Linux踩<b class='flag-5'>坑</b>記錄

    Redis分布式鎖的10個

    這塊代碼是有 的,因為setnx和expire兩個命令是分開寫的,并不是原子操作!如果剛要執(zhí)行完setnx加鎖,正要執(zhí)行expire設置過期時間時,進程crash或者要重啟維護了,那么這個鎖就“長生不老 ”了,別的線程永遠獲取不到鎖。
    的頭像 發(fā)表于 01-10 10:38 ?1069次閱讀

    【避指南】電容耐壓降額裕量不合理導致電容頻繁擊穿

    【避指南】電容耐壓降額裕量不合理導致電容頻繁擊穿
    的頭像 發(fā)表于 11-23 09:04 ?3538次閱讀
    【避<b class='flag-5'>坑</b>指南】電容耐壓降額裕量不合理導致電容頻繁<b class='flag-5'>被</b>擊穿

    問爆的避方法!——天線設計

    天線設計,也是4G模組應用中最容易踩的地方。今天主要分享討論Air700ECQ/EAQ/EMQ系列模組,天線管腳4G天線之間的電路設計和走線規(guī)則。 ? Air700ECQ/EAQ/EMQ模組屬于
    的頭像 發(fā)表于 12-26 16:45 ?1591次閱讀
    <b class='flag-5'>被</b>問爆的避<b class='flag-5'>坑</b>方法!——天線設計

    從踩高效落地:淘寶天貓商品詳情 API 的實操心得

    在電商數據開發(fā)、競品分析等場景中,淘寶天貓商品詳情API是獲取商品數據的核心入口。從初期頻繁報錯、調用低效,如今穩(wěn)定支撐業(yè)務,我沉淀了一套實操邏輯,本文拆解全流程點與落地技巧,幫助開發(fā)者少走彎路
    的頭像 發(fā)表于 02-26 16:51 ?1236次閱讀