1. 程式人生 > 其它 >JAVA併發程式設計(二):執行緒的停止和中斷最佳實踐

JAVA併發程式設計(二):執行緒的停止和中斷最佳實踐

技術標籤:JAVA併發基礎

Java中停止執行緒的原則是什麼?

在Java中,最好的停止執行緒的方式是使用中斷interrupt,但是這僅僅是會通知到被終止的執行緒“你該停止運行了”,被終止的執行緒自身擁有決定權(決定是否、以及何時停止),這依賴於請求停止方和被停止方都遵守一種約定好的編碼規範。
任務和執行緒的啟動很容易。在大多數時候,我們都會讓它們執行直到結束,或者讓它們自行停止。然而,有時候我們希望提前結束任務或執行緒,或許是因為使用者取消了操作,或者服務需要被快速關閉,或者是執行超時或出錯了。
要使任務和執行緒能安全、快速、可靠地停止下來,並不是一件容易的事。Java沒有提供任何機制來安全地終止執行緒。但它提供了中斷( Interruption),這是一種協作機制,能夠使一個執行緒終止另一個執行緒的當前工作。

這種協作式的方法是必要的,我們很少希望某個任務、執行緒或服務立即停止,因為這種立即停止會使共享的資料結構處於不一致的狀態。相反,在編寫任務和服務時可以使用一種協作的方式:當需要停止時,它們首先會清除當前正在執行的工作,然後再結束。這提供了更好的靈活性,因為任務本身的程式碼比發出取消請求的程式碼更清楚如何執行清除工作。
生命週期結束(End-of-Lifecycle)的問題會使任務、服務以及程式的設計和實現等過程變得複雜,而這個在程式設計中非常重要的要素卻經常被忽略。一個在行為良好的軟體與勉強運的軟體之間的最主要區別就是,行為良好的軟體能很完善地處理失敗、關閉和取消等過程。

停止執行緒

  • 使用interrupt來通知執行緒停止,而不是強制停止。
    a. 注意只是通知,並不是讓執行緒立即停止。
    b. 只需要通知執行緒,你需要停止,執行緒通過響應interrupt來在合適的地方停止或者退出執行緒的執行。
  • 為什麼要這樣做呢?
    執行緒在停止時,所使用的資源沒有釋放造成資源浪費甚至BUG,資料處理沒有完成造成資料不一致,這樣的問題往往會令我們頭疼。而如果使用interrupt來通知它,執行緒可以進行停止前的釋放資源,完成必須要處理的資料任務,諸如此類的事情,就會令我們的程式的健壯性提升,也減少了系統出現問題的機率
  • 停止普通執行緒示例
public class RightStopThreadWithoutSleep {
    public static
void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { int num = 0; long start = System.currentTimeMillis(); while (num <= Integer.MAX_VALUE / 2) { if (num % 1000 == 0) { System.out.println(num + " 是10000的倍數!"); } // 注意 如果不interrupted的響應處理,執行緒不會處理interrupt if (Thread.currentThread().isInterrupted()) { System.out.println(Thread.currentThread().getName() + " was interrupted"); break; } num++; } long end = System.currentTimeMillis(); System.out.println("Task was finished! " + (end - start) / 1000.0 + "s"); }); thread.start(); Thread.sleep(2000); thread.interrupt(); } }

• 停止阻塞執行緒
• 如果執行緒在阻塞狀態,比如呼叫sleep()方法時,響應interrupt的方式是丟擲異常。
• 所以停止阻塞執行緒使用try-catch來實現

public class RightStopThreadWithSleep {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            int num = 0;
            long start = System.currentTimeMillis();
            while (num <= 300) {
                if (num % 100 == 0) {
                    System.out.println(num + " 是100的倍數!");
                }
                num++;
                // 注意 如果不interrupted的響應處理,執行緒不會處理interrupt
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println(Thread.currentThread().getName() + " was interrupted");
                    break;
                }
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println(Thread.currentThread().getName() + " thread was interrupted by sleep!");
            }
            long end = System.currentTimeMillis();
            System.out.println("Task was finished! " + (end - start) / 1000.0 + "s");
        });
        thread.start();
        Thread.sleep(500);
        thread.interrupt();
    }
}

0 是100的倍數!
100 是100的倍數!
200 是100的倍數!
300 是100的倍數!
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.imyiren.concurrency.threadcore.stopthread.RightStopThreadWithSleep.lambda$main$0(RightStopThreadWithSleep.java:26)
at java.lang.Thread.run(Thread.java:748)
Thread-0 thread was interrupted by sleep!
Task was finished! 0.505s
Process finished with exit code 0

  • 每個迴圈中都有sleep
  • 如果每個迴圈都有阻塞, 我們就可以不用每個迴圈都判斷一次interrupted了,只需要處理catch的異常即可。
public class RightStopThreadWithSleepInLoop {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            int num = 0;
            long start = System.currentTimeMillis();
            try {
                while (num <= 10000) {
                    if (num % 100 == 0) {
                        System.out.println(num + " 是100的倍數!");
                    }
                    Thread.sleep(10);
                    num++;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println(Thread.currentThread().getName() + " thread was interrupted by sleep!");
            }
            long end = System.currentTimeMillis();
            System.out.println("Task was finished! " + (end - start) / 1000.0 + "s");
        });
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }
}

0 是100的倍數!
100 是100的倍數!
200 是100的倍數!
300 是100的倍數!
400 是100的倍數!
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.imyiren.concurrency.threadcore.stopthread.RightStopThreadWithSleepInLoop.lambda$main$0(RightStopThreadWithSleepInLoop.java:19)
at java.lang.Thread.run(Thread.java:748)
Thread-0 thread was interrupted by sleep!
Task was finished! 5.005s
Process finished with exit code 0

這個地方需要注意一個地方,try-catch的位置,這個不難看出,如果是下列程式碼,則不能interrupt,會死迴圈。

public class CantInterrupt {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            int num = 0;
            long start = System.currentTimeMillis();
            while (num <= 10000) {
                if (num % 100 == 0) {
                    System.out.println(num + " 是100的倍數!");
                }
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println(Thread.currentThread().getName() + " thread was interrupted by sleep!");
                }
                num++;
            }
            long end = System.currentTimeMillis();
            System.out.println("Task was finished! " + (end - start) / 1000.0 + "s");
        });
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }
}

0 是100的倍數!
100 是100的倍數!
200 是100的倍數!
300 是100的倍數!
400 是100的倍數!
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.imyiren.concurrency.threadcore.stopthread.CantInterrupt.lambda$main$0(CantInterrupt.java:17)
at java.lang.Thread.run(Thread.java:748)
Thread-0 thread was interrupted by sleep!
500 是100的倍數!
600 是100的倍數!
700 是100的倍數!
800 是100的倍數!

InterruptedException處理最佳實踐(業務中如何使用?)

• 絕對不應遮蔽中斷請求

  1. 非run()方法直接丟擲interruptedException,不做處理
    • 首先我們不能在業務方法中直接處理掉異常,不能try-catch,需要直接丟擲。
    • 那麼我們在業務方法中處理了這個異常會怎麼樣呢?那麼如果run()方法中有迴圈,則無法退出迴圈。。
    • 最佳實踐:在業務程式碼中有InterruptedException 優先選擇 在方法簽名中丟擲異常,不處理。那麼就會使InterruptedException在run()方法中強制try-catch。如下程式碼
public class RightStopThreadInProd implements Runnable {
    @Override
    public void run() {
        try {
            while (true) {
                System.out.println("business code...");
                // 假設呼叫其他方法
                throwInMethod();
                System.out.println("business code...");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("catch interruptedException handle interrupted! ...");
        }
    }
    private void throwInMethod() throws InterruptedException {
        Thread.sleep(1000);
    }
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightStopThreadInProd());
        thread.start();
        Thread.sleep(500);
        thread.interrupt();
    }
}

business code…
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.imyiren.concurrency.threadcore.stopthread.RightStopThreadInProd.throwInMethod(RightStopThreadInProd.java:28)
at com.imyiren.concurrency.threadcore.stopthread.RightStopThreadInProd.run(RightStopThreadInProd.java:18)
at java.lang.Thread.run(Thread.java:748)
catch interruptedException handle interrupted! …
Process finished with exit code 0

  1. 直接在業務方法中恢復中斷(當業務方法無法丟擲或不想丟擲時)
    • 就是利用中斷機制,呼叫Thread.currentThread().interrupt() 來恢復中斷
public class RightStopThreadInProd2 implements Runnable {
    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            System.out.println("business code...");
            // 假設呼叫其他方法
            reInterrupted();
            System.out.println("business code...");
        }
    }
    private void reInterrupted() {
        try {
            System.out.println("reInterrupted method business! ");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + " reInterrupted interrupt");
            Thread.currentThread().interrupt();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightStopThreadInProd2());
        thread.start();
        Thread.sleep(1500);
        thread.interrupt();
    }
}

business code…
reInterrupted method business!
business code…
business code…
reInterrupted method business!
Thread-0 reInterrupted interrupt
business code…
Process finished with exit code 0

響應中斷的一些方法

  • bject.wait(…) Thraed.sleep(…) Thread.join(…)
  • java.util.concurrent.BlockingQueue.take()/put(E)
  • java.util.concurrent.locks.Lock.lockInterruptibly()
  • java.util.concurrent.CountDownLatch.await()
  • java.util.CyclicBarrier.await()
  • java.util.concurrent.Exchanger.exchange(V)
  • java.nio.channels.InterruptibleChannel的相關方法
  • java.nio.channels.Selector的相關方法

錯誤停止執行緒的方式

• 被棄用的方法:stop()、suspend()、resume()
a. stop方法停止
• 由下程式碼可看到,很有可能,程式碼在計算過程中,最後一部分資料沒被計算進去。
• 程式碼具有偶然性,可能出錯,可能不會出錯。
• 可想如果發生在銀行轉賬過程中,那麼最終的金額對不上。。。這就是個大故障了。。

public class ThreadStop {
    public static void main(String[] args) throws InterruptedException {
        final Data data = new Data();
        Thread thread = new Thread(() -> {
            while (true) {
                int randomInt = (int) (Math.random() * 11);
                int sum = 0, temp;
                for (int i = 1; i < data.nums.length + 1; i++) {
                    temp = randomInt * i;
                    sum += temp;
                    data.nums[i-1] += temp;
                    System.out.println("i=" + i + ", num=" + temp);
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        //...
                    }
                }
                data.total -= sum;
            }
        });
        thread.start();
        Thread.sleep(931);
        thread.stop();
        System.out.println(data);
    }
}
class Data{
    int total = Integer.MAX_VALUE;
    int[] nums = new int[5];
    @Override
    public String toString() {
        int sum = 0;
        for (int i = 0; i < nums.length; i++) {
            sum += nums[i];
        }
        return "Data{" +
                "total=" + total +
                ", nums=" + Arrays.toString(nums) +
                ", sumNums=" + sum +
                ", sum=" + (sum + total) +
                ", Integer.MAX_VALUE=" + Integer.MAX_VALUE +
                '}';
    }
}

a. suspend和resume
• suspend()方法會使得目標執行緒停下來,但卻仍然持有在這之前獲得的鎖定。這樣一來很容造成死鎖。
• 而resume()方法則是用於 恢復通過呼叫suspend()方法而停止執行的執行緒
• 這兩個方法都已被廢棄,所以不推薦使用。
• 用volatile設定boolean標誌位
b. 案例一:可以停止

public class VolatileWrong implements Runnable{
    private volatile boolean canceled = false;
    @Override
    public void run() {
        int num = 0;
        while (!canceled) {
            num++;
            if (num % 100 == 0) {
                System.out.println("num = " + num);
            }
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                //...
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        VolatileWrong volatileWrong = new VolatileWrong();
        Thread thread = new Thread(volatileWrong);
        thread.start();
        Thread.sleep(2345);
        System.out.println("開始停止執行緒...");
        volatileWrong.canceled = true;
    }
}

num = 100
num = 200
開始停止執行緒…
Process finished with exit code 0