啪啪,打臉了!領導說:try-catch必須放在循環體外!

哈嘍,親愛的小夥伴們,技術學磊哥,進步沒得說!歡迎來到新一期的性能解讀系列,我是磊哥。java

今天給你們帶來的是關於 try-catch 應該放在循環體外,仍是放在循環體內的文章,咱們將從性能業務場景分析這兩個方面來回答此問題。bash

不少人對 try-catch 有必定的誤解,好比咱們常常會把它(try-catch)和「低性能」直接畫上等號,但對 try-catch 的本質(是什麼)卻缺乏着最基礎的瞭解,所以咱們也會在本篇中對 try-catch 的本質進行相關的探索框架

img

小貼士:我會盡可能用代碼和評測結果來證實問題,但因爲自己認知的侷限,若有不當之處,請讀者朋友們在評論區指出。性能

性能評測

話很少說,咱們直接來開始今天的測試,本文咱們依舊使用 Oracle 官方提供的 JMH(Java Microbenchmark Harness,JAVA 微基準測試套件)來進行測試。測試

首先在 pom.xml 文件中添加 JMH 框架,配置以下:ui

<!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->
<dependency>
   <groupId>org.openjdk.jmh</groupId>
   <artifactId>jmh-core</artifactId>
   <version>{version}</version>
</dependency>
複製代碼

完整測試代碼以下:spa

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

/**
 * try - catch 性能測試
 */
@BenchmarkMode(Mode.AverageTime) // 測試完成時間
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS) // 預熱 1 輪,每次 1s
@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) // 測試 5 輪,每次 3s
@Fork(1) // fork 1 個線程
@State(Scope.Benchmark)
@Threads(100)
public class TryCatchPerformanceTest {
    private static final int forSize = 1000; // 循環次數
    public static void main(String[] args) throws RunnerException {
        // 啓動基準測試
        Options opt = new OptionsBuilder()
                .include(TryCatchPerformanceTest.class.getSimpleName()) // 要導入的測試類
                .build();
        new Runner(opt).run(); // 執行測試
    }

    @Benchmark
    public int innerForeach() {
        int count = 0;
        for (int i = 0; i < forSize; i++) {
            try {
                if (i == forSize) {
                    throw new Exception("new Exception");
                }
                count++;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return count;
    }

    @Benchmark
    public int outerForeach() {
        int count = 0;
        try {
            for (int i = 0; i < forSize; i++) {
                if (i == forSize) {
                    throw new Exception("new Exception");
                }
                count++;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return count;
    }
}
複製代碼

以上代碼的測試結果爲:線程

img

從以上結果能夠看出,程序在循環 1000 次的狀況下,單次平均執行時間爲:code

  • 循環內包含 try-catch 的平均執行時間是 635 納秒 ±75 納秒,也就是 635 納秒上下偏差是 75 納秒;
  • 循環外包含 try-catch 的平均執行時間是 630 納秒,上下偏差 38 納秒。

也就是說,在沒有發生異常的狀況下,除去偏差值,咱們獲得的結論是:try-catch 不管是在 for 循環內仍是 for 循環外,它們的性能相同,幾乎沒有任何差異orm

img

try-catch的本質

要理解 try-catch 的性能問題,必須從它的字節碼開始分析,只有這樣我能才能知道 try-catch 的本質究竟是什麼,以及它是如何執行的。

此時咱們寫一個最簡單的 try-catch 代碼:

public class AppTest {
    public static void main(String[] args) {
        try {
            int count = 0;
            throw new Exception("new Exception");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
複製代碼

而後使用 javac 生成字節碼以後,再使用 javap -c AppTest 的命令來查看字節碼文件:

➜ javap -c AppTest 
警告: 二進制文件AppTest包含com.example.AppTest
Compiled from "AppTest.java"
public class com.example.AppTest {
  public com.example.AppTest();
    Code:
       0: aload_0
       1: invokespecial #1 // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: iconst_0
       1: istore_1
       2: new           #2 // class java/lang/Exception
       5: dup
       6: ldc           #3 // String new Exception
       8: invokespecial #4 // Method java/lang/Exception."<init>":(Ljava/lang/String;)V
      11: athrow
      12: astore_1
      13: aload_1
      14: invokevirtual #5 // Method java/lang/Exception.printStackTrace:()V
      17: return
    Exception table:
       from    to  target type
           0    12    12   Class java/lang/Exception
}
複製代碼

從以上字節碼中能夠看到有一個異常表:

Exception table:
       from    to  target type
          0    12    12   Class java/lang/Exception
複製代碼

參數說明:

  • from:表示 try-catch 的開始地址;
  • to:表示 try-catch 的結束地址;
  • target:表示異常的處理起始位;
  • type:表示異常類名稱。

從字節碼指令能夠看出,當代碼運行時出錯時,會先判斷出錯數據是否在 fromto 的範圍內,若是是則從 target 標誌位往下執行,若是沒有出錯,直接 gotoreturn。也就是說,若是代碼不出錯的話,性能幾乎是不受影響的,和正常的代碼的執行邏輯是同樣的。

img

業務狀況分析

雖然 try-catch 在循環體內仍是循環體外的性能是相似的,可是它們所代碼的業務含義卻徹底不一樣,例如如下代碼:

public class AppTest {
    public static void main(String[] args) {
        System.out.println("循環內的執行結果:" + innerForeach());
        System.out.println("循環外的執行結果:" + outerForeach());
    }
    
    // 方法一
    public static int innerForeach() {
        int count = 0;
        for (int i = 0; i < 6; i++) {
            try {
                if (i == 3) {
                    throw new Exception("new Exception");
                }
                count++;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return count;
    }

    // 方法二
    public static int outerForeach() {
        int count = 0;
        try {
            for (int i = 0; i < 6; i++) {
                if (i == 3) {
                    throw new Exception("new Exception");
                }
                count++;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return count;
    }
}
複製代碼

以上程序的執行結果爲:

java.lang.Exception: new Exception

at com.example.AppTest.innerForeach(AppTest.java:15)

at com.example.AppTest.main(AppTest.java:5)

java.lang.Exception: new Exception

at com.example.AppTest.outerForeach(AppTest.java:31)

at com.example.AppTest.main(AppTest.java:6)

循環內的執行結果:5

循環外的執行結果:3

能夠看出在循環體內的 try-catch 在發生異常以後,能夠繼續執行循環;而循環外的 try-catch 在發生異常以後會終止循環。

所以咱們在決定 try-catch 到底是應該放在循環內仍是循環外,不取決於性能(由於性能幾乎相同),而是應該取決於具體的業務場景

例如咱們須要處理一批數據,而不管這組數據中有哪個數據有問題,都不能影響其餘組的正常執行,此時咱們能夠把 try-catch 放置在循環體內;而當咱們須要計算一組數據的合計值時,只要有一組數據有誤,咱們就須要終止執行,並拋出異常,此時咱們須要將 try-catch 放置在循環體外來執行。

img

總結

本文咱們測試了 try-catch 放在循環體內和循環體外的性能,發現兩者在循環不少次的狀況下性能幾乎是一致的。而後咱們經過字節碼分析,發現只有當發生異常時,纔會對比異常表進行異常處理,而正常狀況下則能夠忽略 try-catch 的執行。但在循環體內仍是循環體外使用 try-catch,對於程序的執行結果來講是徹底不一樣的,所以咱們應該從實際的業務出發,來決定到 try-catch 應該存放的位置,而非性能考慮

關注公衆號「Java中文社羣」回覆「乾貨」,獲取 50 篇原創乾貨 Top 榜

相關文章
相關標籤/搜索