天天看點

【小知識探究系列三】探究stop、suspend線程中斷方法被淘汰的原因以及替換方案

開頭小詩一首以歌頌留在曆史長河的她們:

《題都城南莊》 崔護

去年今日此門中,人面桃花相映紅。人面不知何處去,桃花依舊笑春風。

我們知道像stop、suspend這幾種中斷或者阻塞線程的方法在較高java版本中已經被标記上了@Deprecated過期标簽,那麼為什麼她們曾經登上了java的曆史舞台而又漸漸的推出了舞台呢,到底是人性的扭曲還是道德的淪喪呢,亦或是她們不思進取被取而代之呢,如果是被取而代之,那麼取而代之的又是何方人也,本文我們将一探究竟。

一、stop的落幕

首先stop方法的作用是什麼呢,用java源碼中的一句注釋來了解一下: Forces the thread to stop executing.,即強制線程停止執行,'Forces’似乎已經透漏出了stop方法的蠻狠無理。那麼我們再看看java開發者是怎們解釋stop被淘汰了的:

This method is inherently unsafe. Stopping a thread with Thread.stop causes it to unlock all of the monitors that it has locked (as a natural consequence of the unchecked

ThreadDeath

exception propagating up the stack). If any of the objects previously protected by these monitors were in an inconsistent state, the damaged objects become visible to other threads, potentially resulting in arbitrary behavior. Many uses of

stop

should be replaced by code that simply modifies some variable to indicate that the target thread should stop running. The target thread should check this variable regularly, and return from its run method in an orderly fashion if the variable indicates that it is to stop running. If the target thread waits for long periods (on a condition variable, for example), the

interrupt

method should be used to interrupt the wait.

我們從中可以看出以下幾點:

1.stop這種方法本質上是不安全的

2.使用Thread.stop停止線程會導緻它解鎖所有已鎖定的螢幕,即直接釋放目前線程已經擷取到的所有鎖,使得目前線程直接進入阻塞狀态

我們舉例來看一下上邊提到的兩點:

public static void main(String[] args) throws InterruptedException {
        Object o1=new Object();
        Object o2=new Object();
        Thread t1=new Thread(()->{
              synchronized (o1)
              {
                  synchronized (o2)
                  {
                      try {
                          System.out.println("t1擷取到鎖");
                          Thread.sleep(5000);
                          System.out.println("t1結束");
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
              }
        });
        t1.start();
        Thread.sleep(1000);
        Thread t2=new Thread(()->{
            synchronized (o1)
            {
                synchronized (o2)
                {
                    try {
                        System.out.println("t2擷取到鎖");
                        Thread.sleep(5000);
                        System.out.println("t2結束");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t2.start();
        t1.stop();
    }
           

運作結果:

【小知識探究系列三】探究stop、suspend線程中斷方法被淘汰的原因以及替換方案

可以看到,當線程t1在擷取到o1和o2兩個鎖開始執行,在還沒有執行結束的時候,主線程調用了t1的stop方法中斷了t1的執行,釋放了t1線程擷取到的所有鎖,中斷後t2擷取到了o1和o2鎖,開始執行直到結束,而t1卻夭折在了sleep的時候,sleep後的代碼沒有執行。是以使用stop我們在不知道線程到底運作到了什麼地方,暴力的中斷了線程,如果sleep後的代碼是資源釋放、重要業務邏輯等比較重要的代碼的話,亦或是其他線程依賴t1線程的運作結果,那直接中斷将可能造成很嚴重的後果。

那麼不建議使用stop中斷線程我們應該怎麼去優雅的結束一個線程呢,我們可以存java開發者的注釋中窺探到一種解決方案:

Many uses of

stop

should be replaced by code that simply modifies some variable to indicate that the target thread should stop running. The target thread should check this variable regularly, and return from its run method in an orderly fashion if the variable indicates that it is to stop running. If the target thread waits for long periods (on a condition variable, for example), the

interrupt

method should be used to interrupt the wait.

可以看到java開發者推薦我們使用以下兩種方法來優雅的停止線程:

1.定義一個變量,由目标線程去不斷的檢查變量的狀态,當變量達到某個狀态時停止線程。代碼舉例如下:

volatile static boolean flag=false;
public static void main(String[] args) throws InterruptedException {
        Object o1=new Object();
        Thread t1=new Thread(()->{
              synchronized (o1)
              {
                  try {
                      System.out.println("t1擷取到鎖");
                      while (!flag)
                          Thread.sleep(5000);//執行業務邏輯
                      System.out.println("t1結束");
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
        });
        t1.start();
        Thread.sleep(1000);
        Thread t2=new Thread(()->{
            synchronized (o1)
            {
                try {
                    System.out.println("t2擷取到鎖");
                    Thread.sleep(5000);//執行業務邏輯
                    System.out.println("t2結束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t2.start();
        flag=true;
    }
           

運作結果:

【小知識探究系列三】探究stop、suspend線程中斷方法被淘汰的原因以及替換方案

2.使用interrupt方法中斷線程。代碼舉例如下:

public static void main(String[] args) throws InterruptedException {
        Object o1=new Object();
        Thread t1=new Thread(()->{
              synchronized (o1)
              {
                  System.out.println("t1擷取到鎖");
                  while (!Thread.currentThread().isInterrupted()) {
                      for (int i = 0; i < 100; i++) {
                          if(i==50)
                              System.out.println();
                          System.out.print(i+" ");
                      }
                      System.out.println();
                  }
                  System.out.println("t1結束");
              }
        });
        t1.start();
        Thread t2=new Thread(()->{
            synchronized (o1)
            {
                try {
                    System.out.println("t2擷取到鎖");
                    Thread.sleep(5000);//執行業務邏輯
                    System.out.println("t2結束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t2.start();
        t1.interrupt();
    }
           

運作結果:

【小知識探究系列三】探究stop、suspend線程中斷方法被淘汰的原因以及替換方案

我們用while (!Thread.currentThread().isInterrupted())來不斷判斷目前線程是否被中斷,中斷的話則讓線程自然消亡并釋放鎖。可以看到調用interrupt方法後并不會像stop那樣暴力的中斷線程,會等到目前運作的邏輯結束後再檢查是否中斷,非常的優雅。【注:運作舉例代碼可能不會列印出數字,這是因為t1線程運作到while(!Thread.currentThread().isInterrupted())時,主線程已經調了interrupt方法,是以多次運作可能會列印出數字】。

二、suspend的落幕

suspend方法的作用是挂起某個線程直到調用resume方法來恢複該線程,但是調用了suspend方法後并不會釋放被挂起線程擷取到的鎖,正因如此就給suspend和resume這哥倆貼上了容易引發死鎖的标簽,當然這也正是導緻suspend和resume退出曆史舞台的罪魁禍首。同樣我們看看java開發者為suspend的淘汰給出的理由:

This method has been deprecated, as it is inherently deadlock-prone. If the target thread holds a lock on the monitor protecting a critical system resource when it is suspended, no thread can access this resource until the target thread is resumed. If the thread that would resume the target thread attempts to lock this monitor prior to calling

resume

, deadlock results. Such deadlocks typically manifest themselves as “frozen” processes.

從中我們可以得出以下結論:

1.suspend具有天然的死鎖傾向

2.當某個線程被suspend後,該線程持有的鎖不會被釋放,其他線程也就不能通路這些資源

3.suspend某個線程後,如果在resume的過程中出現異常導緻resume方法執行失敗,則lock無法釋放,導緻死鎖

接下來模拟一下由suspend引起的死鎖場景,Talk is cheap,show my code:

public static void main(String[] args) throws InterruptedException {
        Object o1=new Object();
        Object o2=new Object();
        Thread t1=new Thread(()->{
              synchronized (o1)
              {
                  System.out.println("t1擷取到o1鎖開始執行");
                  try {
                      Thread.sleep(5000);//模拟執行業務邏輯
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  System.out.println("t1執行結束");
              }
        });
        t1.start();
        Thread t2=new Thread(()->{
            synchronized (o2)
            {
                System.out.println("t2擷取到o2開始執行");
                try {
                    Thread.sleep(2000);//執行耗時業務
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1)
                {
                    System.out.println("t2擷取到o1鎖開始繼續執行");
                }
                System.out.println("t2執行結束");
            }
        });
        t2.start();

        Thread.sleep(1000);
        t1.suspend();
        //假設抛出了一個未知異常
        int i=1/0;
        t1.resume();
    }
           

運作結果:

【小知識探究系列三】探究stop、suspend線程中斷方法被淘汰的原因以及替換方案

可以看到,整個程式卡的死死的,在調用resume恢複t1線程之前抛出了一個未知異常,導緻t1一直挂起進而無法釋放o1鎖,而t2需要擷取到o1鎖後才能繼續執行,但苦苦等待,奈何o1被t1拿捏的死死的,從此整個程式就陷入了無盡的等待中----死鎖。

具體的替換方法可以參考這篇文章:如何防止線上程阻塞喚醒是死鎖

參考:

Why are Thread.stop, Thread.suspend and Thread.resume Deprecated?

如何防止線上程阻塞喚醒是死鎖