JAVA進階篇(13)— 正則表達式的性能優化(正則表達式導致CPU飆升)

1、正則表達式基本使用

正則表達式 — 基本語法
正則表達式 — 常用案例

測試demo:

    private static void test2() {
        String regular = "^[\\u4e00-\\u9fa5_a-zA-Z0-9]+$";
        String testStr = "a_行政村行政村選擇充血出現在出effg+現在重置線程這需求行政村行政村選擇充血自產自銷區+";
        Pattern p = Pattern.compile(regular);
        Matcher m = p.matcher(testStr);
        boolean res = m.find();
        System.out.println(res);
    }

2、正則表達式三種算法

Java 語言使用的正則表達式執行引擎是 NFA (Non-deterministic finite automaton) 非確定型有窮自動機,這種引擎的特點是:功能強大、單存在回溯機制導致執行效率慢(回溯嚴重時可以導致機器 CPU 使用率 100%,直接卡死機器)。

測試代碼:

    private static void test1() {
        String testStr = "a_行政村行政村選擇充血出現在出effg現在重置線程這需求行政村行政村選擇充血自產自銷區";

        String regular_1 = "^[\\u4e00-\\u9fa5_a-zA-Z0-9]+$";    //貪婪模式
        String regular_2 = "^[\\u4e00-\\u9fa5_a-zA-Z0-9]++$";   //懶惰模式
        String regular_3 = "^[\\u4e00-\\u9fa5_a-zA-Z0-9]+?$";   //獨占模式

        List<String> regulars = new ArrayList<>();
        regulars.add(regular_1);
        regulars.add(regular_2);
        regulars.add(regular_3);

        for (String regular : regulars) {
            long start, end;
            start = System.currentTimeMillis();
            Pattern p = Pattern.compile(regular);
            Matcher m = p.matcher(testStr);
            boolean res = m.find();

            end = System.currentTimeMillis();
            System.out.println("結果:" + JSON.toJSONString(res) + ",執行時間:" + (end - start) + "(ms)");
        }
    }

執行結果:

結果:true,執行時間:3(ms)
結果:true,執行時間:1(ms)
結果:true,執行時間:0(ms)

可以明顯看到,雖然實現了相同的匹配功能,但效率卻有所區別,原因在于這三種寫法定義了正則表達式的三種匹配邏輯,我們來逐一說明:

正則表達式:ef{1,3}g
待匹配的字符串:effg

2.1 貪婪模式(默認)

語法:ef{1,3}g

原理:貪婪模式是正則表達式的默認匹配方式,在該模式下,對于涉及數量的表達式,正則表達式會盡量匹配更多的內容。

說明:我用模型圖來演示一下匹配邏輯:

image.png

到第二步的時候其實已經滿足第二個條件f{1,3},但我們說過貪婪模式會盡量匹配更多的內容,所以依然停在第二個條件繼續遍歷字符串

image.png

注意看第四步,字符g不滿足匹配條件f{1,3},這個時候會觸發回溯機制:指針重新回到第三個字符f處

image.png

關于回溯機制:

回溯是造成正則表達式效率問題的根本原因,每次匹配失敗,都需要將之前比對過的數據復位且指針調回到數據的上一位置,想要優化正則表達式的匹配效率,減少回溯是關鍵。

回溯之后,繼續從下一個條件以及下一個字符繼續匹配,直到結束。

2.2 懶惰模式

語法: ef{1,3}?g

原理:與貪婪模式相反,懶惰模式會盡量匹配更少的內容。

說明:

image.png

到第二步的時候,懶惰模式會認為已經滿足條件f{1,3},所以會直接判斷下一條件。

image.png

注意,到這步因為不滿足匹配條件,所以觸發回溯機制,將判斷條件回調到上一個。

image.png

回溯之后,繼續從下一個條件以及下一個字符繼續匹配,直到結束。

image.png

2.3 獨占模式(推薦)

語法:ef{1,3}+g

原理:獨占模式應該算是貪婪模式的一種變種,它同樣會盡量匹配更多的內容,區別在于在匹配失敗的情況下不會觸發回溯機制,而是繼續向后判斷,所以該模式效率最佳。

說明:

image.png

2.4 三種模式表達式

貪婪模式 懶惰模式 獨占模式
X? X?? X?+
X* X*? X*+
X+ X+? X++
X{n} X{n}? X{n}+
X{n,} X{n,}? X{n,}+
X{n,m} X{n,m}? X{n,m}+

3 優化建議

建議1:推薦使用獨占模式來優化正則比表達式,格式^[允許字符集]+ 。
建議2:推薦將Pattern 緩存下來,避免反復編譯Pattern。

static final Pattern HEAVY_REGEX = Pattern.compile("(((X)*Y)*Z)*");

建議3:優化正則中的分支選擇

通過上面對正則表達式匹配邏輯的了解,我們不難想到,由于回溯機制的存在,帶有分支選擇的正則表達式必然會降低匹配效率

String testStr = "abbdfg";
String regular = "(aab|aba|abb)dfg";

在這個例子中,"aab"并未匹配,于是回溯到字符串的第一個元素重新匹配第二個分支"aba",以此類推,直到判斷完所有分支,效率問題可想而知。

如果分支中存在公共前綴,可以進行提取:

String regular = "a(ab|ba|bb)dfg";

這樣首先減少了公共前綴的判斷次數,其次降低了分支造成的回溯頻率,相比之下效率有所提升。

文章參考

正則表達式的三種模式:貪婪模式、懶惰模式、獨占模式

Java性能調優--代碼篇:優化正則表達式的匹配效率

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容