JAVA併發程式設計(二):執行緒的停止和中斷最佳實踐
技術標籤:JAVA併發基礎
Java中停止執行緒的原則是什麼?
在Java中,最好的停止執行緒的方式是使用中斷interrupt,但是這僅僅是會通知到被終止的執行緒“你該停止運行了”,被終止的執行緒自身擁有決定權(決定是否、以及何時停止),這依賴於請求停止方和被停止方都遵守一種約定好的編碼規範。
任務和執行緒的啟動很容易。在大多數時候,我們都會讓它們執行直到結束,或者讓它們自行停止。然而,有時候我們希望提前結束任務或執行緒,或許是因為使用者取消了操作,或者服務需要被快速關閉,或者是執行超時或出錯了。
要使任務和執行緒能安全、快速、可靠地停止下來,並不是一件容易的事。Java沒有提供任何機制來安全地終止執行緒。但它提供了中斷( Interruption),這是一種協作機制,能夠使一個執行緒終止另一個執行緒的當前工作。
生命週期結束(End-of-Lifecycle)的問題會使任務、服務以及程式的設計和實現等過程變得複雜,而這個在程式設計中非常重要的要素卻經常被忽略。一個在行為良好的軟體與勉強運的軟體之間的最主要區別就是,行為良好的軟體能很完善地處理失敗、關閉和取消等過程。
停止執行緒
- 使用interrupt來通知執行緒停止,而不是強制停止。
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處理最佳實踐(業務中如何使用?)
• 絕對不應遮蔽中斷請求
- 非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
- 直接在業務方法中恢復中斷(當業務方法無法丟擲或不想丟擲時)
• 就是利用中斷機制,呼叫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