Java停止(終止)執行緒詳解版

2020-07-16 10:04:40
停止執行緒是在多執行緒開發中很重要的技術點,掌握此技術可以對執行緒的停止進行有效的處理。停止執行緒在 Java 語言中並不像 break 語句那樣乾脆,需要一些技巧性的處理。

使用 Java 內建支援多執行緒的類設計多執行緒應用是很常見的事情,然而,多執行緒給開發人員帶來了一些新的挑戰,如果處理不好就會導致超出預期的行為並且難以定位錯誤。

本節將討論如何更好地停止一個執行緒。停止一個執行緒意味著線上程處理完任務之前停掉正在做的操作,也就是放棄當前的操作。雖然這看起來非常簡單,但是必須做好防範措施,以便達到預期的效果。

停止一個執行緒可以使用 Threadstop() 方法,但最好不用它。雖然它確實可以停止一個正在執行的執行緒,但是這個方法是不安全的,而且已被棄用作廢了,在將來的 Java 版本中,這個方法將不可用或不被支援。

大多數停止一個執行緒的操作使用 Thread.interrupt() 方法,儘管方法的名稱是“停止,中止”的意思,但這個方法不會終止一個正在執行的執行緒,還需要加入一個判斷才可以完成執行緒的停止。關於此知識點在後面有專門的章節進行介紹。

在 Java 中有以下 3 種方法可以終止正在執行的執行緒:
  1. 使用退出標識,使執行緒正常退出,也就是當 run() 方法完成後執行緒終止。
  2. 使用 stop() 方法強行終止執行緒,但是不推薦使用這個方法,因為 stop() 和 suspend() 及 resume() 一樣,都是作廢過期的方法,使用它們可能產生不可預料的結果。
  3. 使用 interrupt() 方法中斷執行緒。

停止不了的執行緒

interrupt() 方法的作用是用來停止執行緒,但 intermpt() 方法的使用效果並不像迴圈結構中 break 語句那樣,可以馬上停止迴圈。呼叫 intermpt() 方法僅僅是在當前執行緒中打了一個停止的標記,並不是真的停止執行緒。

例 1

下面通過一個案例演示 interrupt() 方法停止執行緒的用法。案例用到的執行緒非常簡單,僅僅是實現輸出從 1~10000 的整數,程式碼如下:
package ch14;
public class MyThread13 extends Thread
{
    @Override 
    public void run()
    { 
        super.run(); 
        for (int i=0;i<10000;i++)
        { 
            System.out.println("i="+(i+1)); 
        } 
    } 
}

在呼叫 intermpt() 方法停止 MyThread13 執行緒之前,首先進行了一個 100 毫秒的休眠。主執行緒的程式碼如下:
package ch14;
public class Test17
{
    public static void main(String[] args)
    { 
        try
        { 
            MyThread13 thread=new MyThread13();      //建立MyThread13執行緒類範例
            thread.start();    //啟動執行緒
            Thread.sleep(100);    //延時100毫秒
            thread.interrupt();    //停止執行緒
        }
        catch(InterruptedException e)
        { 
            System.out.println("main catch"); 
            e.printStackTrace(); 
        } 
    }
}

主執行緒的執行結果如下所示。從中可以看到,雖然在延時 100 毫秒後呼叫 intermpt() 方法停止了 thread 執行緒,但是該執行緒仍然執行完成輸出 10000 行資訊。
i=1
i=2
...
i=9999
i=10000

判斷執行緒是不是停止狀態

在介紹如何停止執行緒的知識點前,先來看一下如何判斷執行緒的狀態是不是停止的。在 Java 的 SDK 中,Thread.java 類裡提供了兩種方法。
  1. this.interrupted():測試當前執行緒是否已經中斷。
  2. this.islnterrupted():測試執行緒是否已經中斷。

那麼這兩個方法有什麼區別呢?先來看看 this.intermpted() 方法的解釋:測試當前執行緒是否已經中斷,當前執行緒是指執行 this.interrupted() 方法的執行緒。為了對此方法有更深入的了解,下面通過一個案例進行說明。

例 2

假設 MyThread14 執行緒類的程式碼如下:
package ch14;
public class MyThread14 extends Thread
{
    @Override 
    public void run()
    { 
        super.run(); 
        for(int i=0;i<10000;i++)
        { 
            System.out.println("i="+(i+1)); 
        } 
    } 
}

主執行緒的程式碼如下:
package ch14;
public class Test18
{
    public static void main(String[] args)
    {
        try
        {
            MyThread14 thread=new MyThread14();
            thread.start();    //啟動執行緒
            Thread.sleep(100);    //延時100毫秒
            thread.interrupt();    //停止執行緒
            //Thread.currentThread().interrupt();
            System.out.println("是否停止1?="+thread.interrupted());
            System.out.println("是否停止2?="+thread.interrupted());
        }
        catch(InterruptedException e)
        {
            System.out.println("main catch");
            e.printStackTrace();
        }
        System.out.println("end!");
    }
}

程式執行後的結果如下所示。
i=1
i=2
...
i=9999
i=10000
是否停止1?=false
是否停止2?=false
end!

在主執行緒中是在 thread 物件上呼叫以下程式碼來停止 thread 物件所代表的執行緒。
thread.interrupt();

後面又使用以下程式碼來判斷 thread 物件所代表的執行緒是否停止。
System.out.println("是否停止 1 ? ="+thread.interrupted());
System.out.println("是否停止 2 ? ="+thread.interrupted());

從控制台列印的結果來看,執行緒並未停止,這也就證明了 interrupted() 方法的解釋:測試當前執行緒是否已經中斷。這個“當前執行緒”是 main,它從未中斷過,所以列印的結果是兩個 false。

那麼如何使 main 執行緒產生中斷效果呢?再來看一下如下的程式碼:
public static void main(String[] args)
{
    Thread.currentThread().interrupt();
    System.out.println(" 是否停止 1 ? ="+Thread.interrupted());
    System.out.println(" 是否停止 2 ? ="+Thread.interrupted());
    System.out.println("end!");
}

程式執行後的結果如下所示。
是否停止 1 ? =true
是否停止 2 ? =false end!

從上述的結果來看,intermpted() 方法的確用來判斷出當前執行緒是不是停止狀態。但為什麼第二個布林值是 false 呢?檢視一下官方幫助文件中對 interrupted() 方法的解釋如下(斜體顯示):
測試當前執行緒是否已經中斷。執行緒的中斷狀態由該方法清除。換句話說,如果連續兩次呼叫該方法,則第二次呼叫將返回 false(在第一次呼叫已清除了其中斷狀態之後,且第二次呼叫檢驗完中斷狀態前,當前執行緒再次中斷的情況除外)。

文件已經解釋得很詳細,intermpted() 方法具有清除狀態的功能,所以第二次呼叫 interrupted() 方法返回的值是 false。

介紹完 interrupted() 方法後再來看一下 isInterrupted() 方法。isInterrupted() 方法的宣告如下:
public boolean isInterrupted()

從宣告中可以看出 isIntermpted() 方法不是 static 的。仍然以 MyThread14 執行緒為例,這裡使用 isInterrupted() 方法來判斷執行緒是否停止,具體程式碼如下:
package ch14;
public class Test18
{
    public static void main(String[] args)
    {
        try
        {
            MyThread14 thread=new MyThread14();
            thread.start();
            Thread.sleep(100);
            thread.interrupt();
            System.out.println("是否停止1?="+thread.isInterrupted());
            System.out.println("是否停止2?="+thread.isInterrupted());
        }
        catch(InterruptedException e)
        {
            System.out.println("main catch");
            e.printStackTrace();
        }
        System.out.println("end!");
    }
}

程式執行結果如下所示。
i=498
是否停止1?=true
i=499
是否俜止2?=true
i=500
end!
i=501
i=502

從程式的執行結果中可以看到,isInterrupted() 方法並未清除狀態標識,所以列印了兩個 true。經過上面範例的驗證總結一下這兩個方法。
  1. this.interrupted():測試當前執行緒是否已經是中斷狀態,執行後具有將狀態標識清除為 false 的功能。
  2. this.islnterrupted():測試執行緒 Thread 物件是否已經是中斷狀態,但不清除狀態標識。

異常法停止執行緒

有了前面學習過的知識,就可線上程中用 for 語句來判斷執行緒是否為停止狀態,如果是停止狀態,則後面的程式碼不再執行。

例 3

下面的執行緒類 MyThread15 演示了線上程中使用 for 迴圈,並在迴圈中呼叫 intermpted() 方法判斷執行緒是否停止。
package ch14;
public class MyThread15 extends Thread
{
    @Override
    public void run()
    {
        super.run();
        for(int i=0;i<500000;i++)
        {
            if(this.interrupted())
            {    //如果當前執行緒處於停止狀態
                System.out.println("已經是停止狀態了!我要退出了!");
                break;
            }
            System.out.println("i="+(i+1));
        }
    }
}

接下來編寫啟動 MyThread15 執行緒的程式碼,主執行緒程式碼如下:
package ch14;
public class Test19
{
    public static void main(String[] args)
    {
        try
        { 
            MyThread15 thread=new MyThread15(); 
            thread.start();    //啟動執行緒
            Thread.sleep(2000);    //延時2000毫秒
            thread.interrupt();    //停止執行緒
        }
        catch(InterruptedException e)
        {    //捕捉執行緒停止異常
            System.out.println("main catch"); 
            e.printStackTrace(); 
        } 
        System.out.println("end!");    //主執行緒結束時輸出
    }
}

上述程式碼啟動 MyThread15 執行緒後延時 2000 毫秒,之後將執行緒停止。為避免主執行緒崩潰使用 catch 捕捉了 InterruptedException 異常,此時會輸出“main catch”。在主執行緒執行結束後會輸出“end!”。程式執行的輸出結果如下所示。
......
i=271597
i=271598
已經是停止狀態了!我要退出了!
end!

從程式執行的結果可以看到,在範例中雖然停止了執行緒,但如果 for 語句下面還有語句,還是會繼續執行的。

下面對 MyThread15 執行緒進行修改,如下所示是 run() 方法的程式碼:
public void run()
{ 
    super.run(); 
    for(int i=0;i<500000;i++)
    { 
        if(this.interrupted())
        { 
            System.out.println("已經是停止狀態了!我要退出了!"); 
            break; 
        } 
        System.out.println("i="+(i+1)); 
    } 
    System.out.println("我被輸出,如果此程式碼是for又繼續執行,執行緒並未停止!"); 
}

此時的執行效果如下所示,說明執行緒仍然在繼續執行。
......
i=233702
i=233703
end!
已經是停止狀態了!我要退出了!
我被輸出,如果此程式碼是for又繼續執行,執行緒並未停止!

那該如何解決執行緒停止後,語句仍然繼續執行的問題呢?解決的辦法是線上程中捕捉執行緒停止異常,如下為修改後的 run() 方法程式碼。
public void run()
{ 
    super.run(); 
    try
    { 
        for(int i=0;i<500000;i++)
        { 
            if(this.interrupted())
            { 
                System.out.println("已經是停止狀態了!我要退出了!"); 
                throw new InterruptedException(); 
            } 
            System.out.println("i=" + (i + 1)); 
        } 
        System.out.println("我在for下面"); 
    }
    catch(InterruptedException e)
    { 
        System.out.println("進MyThread15.java類run方法中的catch了!"); 
        e.printStackTrace(); 
    } 
}

再次執行程式,當執行緒處於停止狀態後,如果 for 迴圈中的程式碼繼續執行將會拋出 InterruptedException 異常,執行結果如下所示。
......
i=251711
i=251712
i=251713
已經是停止狀態了!我要退出了!
end!
進MyThread15.java類run方法中的catch了!
java.lang.InterruptedException
    at text.MyThread.run(MyThread.java:16)

在休眠中停止

如果執行緒在 sleep() 狀態下停止,會是什麼效果呢?

例 4

下面通過一個案例來演示這種情況。如下所示為案例中使用的 MyThread16 執行緒類程式碼。
package ch14;
public class MyThread16 extends Thread
{
    @Override
    public void run()
    {
        super.run();
        try
        {
            System.out.println("run begin");
            Thread.sleep(200000);
            System.out.println("run end");
        }
        catch(InterruptedException e)
        {
            System.out.println("在休眠中被停止!進入catch!"+this.isInterrupted());
            e.printStackTrace();
        }
    }
}

呼叫 MyThread16 執行緒的主執行緒程式碼如下:
package ch14;
public class Test20
{
    public static void main(String[] args)
    {
        try
        {
            MyThread16 thread=new MyThread16();
            thread.start();
            Thread.sleep(200);
            thread.interrupt();
        }
        catch(InterruptedException e)
        {
            System.out.println("main catch");
            e.printStackTrace();
        }
        System.out.println("end!");
    }
}

在上述程式碼中啟動 MyThread16 執行緒後休眠了 200 毫秒,之後呼叫 interrupt() 方法停止執行緒,執行結果如下所示。
run begin
end!
在休眠中被停止!進入catch!false
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at text.MyThread.run(MyThread.java:12)

從執行結果來看,如果在休眠狀態下停止某一執行緒則會拋出進入 InterruptedException 異常,所以會進入 catch 語句塊清除停止狀態值,使之變成 false。

例 5

這個範例是先休眠再停止執行緒,下面再編寫一個案例來演示先停止再休眠執行緒的情況。案例使用的 MyThread17 執行緒類程式碼如下:
package ch14;
public class MyThread17 extends Thread
{
    @Override
    public void run()
    {
        super.run();
        try
        {
            for(int i=0;i<1000;i++)
            {
                System.out.println("i="+(i+1));
            }
            System.out.println("run begin");
            Thread.sleep(200);
            System.out.println("run end");
        }
        catch(InterruptedException e)
        {
            System.out.println("先停止,再遇到了sleep!進入catch!");
            e.printStackTrace();
        }
    }
}

使用 MyThread17 執行緒的主執行緒程式碼如下:
package ch14;
public class Test21
{
    public static void main(String[] args)
    {
        MyThread17 thread=new MyThread17();
        thread.start();
        thread.interrupt();
        System.out.println("end!");
    }
}

在上述程式碼中啟動 MyThread17 執行緒後沒有進行延時,馬上呼叫 interrupt() 方法進行停止執行緒,但是在 MyThread17 執行緒中有一個 200 毫秒的延時。執行程式後,首先會看到下所示的輸出,說明主執行緒執行完畢。
end!
i=1
i=2
i=3
i=4
i=5
i=6
......

稍等片刻後,將會看到如下所示的異常,說明執行緒停止了。
......
i=999
i=1000
run begin
先停止,再遇到了sleep!進入catch!
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at text.MyThread.run(MyThread.java:16)

強制停止執行緒

呼叫 stop() 方法可以在任意情況下強制停止一個執行緒。下面通過一個案例來演示 stop() 停止執行緒的方法。
package ch14;
public class MyThread18 extends Thread
{
    private int i=0;
    @Override
    public void run()
    {
        try
        {
            while (true)
            {
                i++;
                System.out.println("i=" + i);
                Thread.sleep(1000);
            }
        }
        catch(InterruptedException e)
        {
            //TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

如上述程式碼所示,MyThread18 執行緒中包含一個死迴圈,該迴圈每隔 1000 毫秒執行一次,每次將 i 的值遞增 1 並輸出。

呼叫 MyThread18 執行緒的主執行緒程式碼如下:
package ch14;
public class Test22
{
    @SuppressWarnings("deprecation")
    public static void main(String[] args)
    {
        try
        {
            MyThread18 thread=new MyThread18();
            thread.start();
            Thread.sleep(8000);
            thread.stop();
        }
        catch(InterruptedException e)
        {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

如上述程式碼所示,MyThread18 執行緒在啟動後有一個 8000 毫秒的延時,在這段時間內會迴圈 9 次,之後 stop() 方法被執行從而執行緒停止。執行後的輸出如下所示。
i=1
i=2
i=3
i=4
i=5
i=6
i=7
i=8
i=9

注意:呼叫 stop() 方法時會丟擲 java.lang.ThreadDeath 異常,但在通常情況下,此異常不需要顯式地捕捉。

釋放鎖的不良後果

從 JDK 1.6 以後 stop() 方法已經被作廢,因為如果強制讓執行緒停止則有可能使一些清理性的工作得不到完成。另外一個情況就是對鎖定的物件進行了“解鎖”,導致資料得不到同步的處理,出現資料不一致的問題。

使用 stop() 釋放鎖將會給資料造成不一致性的結果。如果出現這樣的情況,程式處理的資料就有可能遭到破壞,最終導致程式執行的流程錯誤,一定要特別注意。

例 6

下面通過一個案例來演示這種情況。案例使用了一個名為 SynchronizedObject 的實體類,該類程式碼如下:
package ch14;
public class SynchronizedObject
{
    private String username="root";
    private String password="root";
    public String getUsername()
    {
        return username;
    }
    public void setUsername(String username)
    {
        this.username=username;
    }
    public String getPassword()
    {
        return password;
    }
    public void setPassword(String password)
    {
        this.password=password;
    }
    synchronized public void printString(String username,String password)
    {
        try
        {
            this.username=username;
            Thread.sleep(100000);
            this.password=password;
        }
        catch(InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

如上述程式碼所示,SynchronizedObject 類包含使用者名稱和密碼兩個成員,printString() 方法用於對這兩個成員進行賦值,但是在設定密碼之前有一個休眠時間。

下面編寫一個執行緒來對 SynchronizedObject 類進行範例化,並呼叫 printString() 方法。執行緒程式碼如下:
package ch14;
public class MyThread19 extends Thread
{
    private SynchronizedObject object;
    public MyThread19(SynchronizedObject object)
    {
        super();
        this.object=object;
    }
    @Override
    public void run()
    {
        object.printString("admin","123456");
    }
}

接下來編寫主執行緒程式碼如下:
package ch14;
public class Test23
{
    public static void main(String[] args)
    { 
        try
        { 
            SynchronizedObject object=new SynchronizedObject(); 
            MyThread19 thread=new MyThread19(object); 
            thread.start(); 
            Thread.sleep(500); 
            thread.stop(); 
            System.out.println("使用者名稱:"+object.getUsername());
            System.out.println("密碼:"+object.getPassword()); 
        }
        catch(InterruptedException e)
        { 
            e.printStackTrace();
        } 
    } 
}

在上述程式碼中建立一個 SynchronizedObject 類範例,並將該範例作為引數傳遞給 MyThread19 執行緒。MyThread19 執行緒啟動後將立即呼叫 object.printString('fadminn,"123456") 方法,而在 printString() 方法內有一個較長時間的休眠。該休眠時間大於主執行緒的休眠時間,所以主執行緒會繼續往下執行,當執行到 stop() 方法時執行緒被強制停止。

程式最後的執行結果如下所示。
使用者名稱:admin
密碼:root

由於 stop() 方法已經在中被標明是“作廢/過期”的方法,顯然它在功能上具有缺陷,所以不建議在程式中使用 stop() 方法。

使用 return 停止執行緒

除了上面介紹的方法外,還可以將 intermpt() 方法與 return 結合使用來實現停止執行緒的效果。

例 7

下面通過一個案例來演示這種情況。如下所示為案例中使用 MyThread20 執行緒類的程式碼。
package ch14;
public class MyThread20 extends Thread
{
    @Override
    public void run()
    {
        while (true)
        {
            if (this.isInterrupted())
            {
                System.out.println("停止了!");
                return;
            }
            System.out.println("timer="+System.currentTimeMillis());
        }
    }
}

呼叫 MyThread20 執行緒的主執行緒程式碼如下:
package ch14;
public class Test24
{
    public static void main(String[] args) throws InterruptedException
    {
        MyThread20 t=new MyThread20();
        t.start();
        Thread.sleep(2000);
        t.interrupt();
    }
}

程式執行後的結果如下所示。
......
timer=1540977194784
timer=1540977194784
timer=1540977194784
timer=1540977194784
timer=1540977194784
停止了!

從程式的執行結果中可以看到成功停止了執行緒,不過還是建議使用“拋異常”的方法來實現執行緒的停止,因為在 catch 塊中還可以將異常向上拋,使執行緒停止的事件得以傳播。