併發編程之 Thread 類過時方法和經常使用方法

前言

在 Java 剛誕生時,Thread 類就已經有了不少方法,但這些方法因爲一些緣由(有一些明顯的bug或者設計不合理)有些已經廢棄了,可是他們的方法名倒是很是的好,真的是浪費。咱們在進行併發必編程的時候必定要注意這些。java

  1. 過時方法1----- stop 方法
  2. 過時方法2------suspend 方法和 resume 方法
  3. 經常使用方法1------線程中斷方法 interrupt,isInterrupted,static interrupted
  4. 經常使用方法2------等待線程結束 join 方法
  5. 經常使用方法3------線程讓出時間片 yield 方法

1. 過時方法1----- stop 方法

JDK 源碼:面試

該方法被定義了 @Deprecated 註解,並在註釋中說明了爲何廢棄:編程

該方法具備固有的不安全性。用 Thread.stop 來終止線程將釋放它已經鎖定的全部監視器(做爲沿堆棧向上傳播的未檢查 ThreadDeath 異常的一個天然後果)。若是之前受這些監視器保護的任何對象都處於一種不一致的狀態,則損壞的對象將對其餘線程可見,這有可能致使任意的行爲。stop 的許多使用都應由只修改某些變量以指示目標線程應該中止運行的代碼來取代。目標線程應按期檢查該變量,而且若是該變量指示它要中止運行,則從其運行方法依次返回。若是目標線程等待很長時間(例如基於一個條件變量),則應使用 interrupt 方法來中斷該等待。api

很官方對不對?仍是用樓主的話來解釋一下吧。最重要的緣由就i是 stop 太粗魯了,強行把執行到一半的線程終止,引發數據不一致。好比有些數據處理到一半,該方法就強行中止線程,致使數據不一致。安全

可以使用一個條件判斷來代替此功能,好比設置一個變量,若是這個變量是ture 則跳出循環,結束線程的執行。反正不要使用該方法就對了。併發

2. 過時方法2------suspend 方法和 resume 方法

JDK 源碼: 函數

suspend 方法

resume 方法

這兩個方法都被標註爲過時,樓主解釋一下爲何不能使用。測試

suspend 方法的做用是掛起方法,而 resume 方法的做用是繼續執行,能夠說這兩個方法是對應的,先掛起,而後繼續執行。這兩個動做是相反的。可是爲何不建議使用呢?緣由就彷佛 suspend 方法在致使線程暫停的同時,並不會釋聽任何鎖資源。此時,其餘任何線程想要訪問被他暫用的鎖時,都會被牽連,致使沒法正常運行。知道對應的 resume 方法被調用,被掛起的線程才能繼續。可是,請注意,這裏嚴格要求 resume 方法在 suspend 方法後面執行,若是 resume 方法意外的在suspend 方法以前執行了,就會致使死鎖,該線程擁有不會恢復。spa

最坑的是,當產生死鎖的時候,你確定會使用 jps 命令和 jstack 命令去查看死鎖。可是你會發現你根本找不到,由於這個線程的狀態是 Rannable。你根本沒法判斷是哪一個線程被掛起了,因此,該方法必定要廢棄。命令行

好比樓主寫了一個例子:

package cn.think.in.java.two;

public class BadSuspend {

  static Object u = new Object();
  static ChangeObjectThread t1 = new ChangeObjectThread("t1");
  static ChangeObjectThread t2 = new ChangeObjectThread("t2");


  static class ChangeObjectThread extends Thread {

    public ChangeObjectThread(String name) {
      super.setName(name);
    }

    public void run() {
      synchronized (u) {
        System.out.println("in " + getName());
        // 暫停
        Thread.currentThread().suspend();
      }
    }
  }

  public static void main(String[] args) throws InterruptedException {
    t1.start();
    Thread.sleep(100);
    // 此時 t1 已經暫停
    t2.start();
    // t1 恢復
    t1.resume();
    // t2 這時恢復,可是 t2在恢復以後進入了暫停,致使死鎖。
    // 除非使用 sleep 讓 t2 先暫停就能夠。
// Thread.sleep(100);
    t2.resume();
    t1.join();
    t2.join();

  }

}

複製代碼

該方法會發生死鎖。然而咱們在命令行中使用 jstack 命令查看時,會發現該線程狀態是 Rannable。

死鎖 狀態 倒是 Rannable

所以在之後的併發編程必定不要使用該方法。

3. 經常使用方法1------線程中斷方法 interrupt,isInterrupted,static interrupted

關於線程中斷還有3個方法:

public void interrupt() public boolean isInterrupted() public static boolean interrupted() 複製代碼

public void interrupt() 做用:中斷線程,也就是設置中斷標記,注意,是設置標記,不會中斷。 public boolean isInterrupted() 做用:判斷線程是否中斷 static boolean Thread interrupted 做用:判斷是否中斷,並清除當前中斷狀態。

咱們解釋解釋這三個方法: 在 java 中,線程中斷是一種重要的線程協做機制。能夠用來代替 stop方法,嚴格來說, 線程中斷並不會使線程當即退出, 而是給線程發一個通知,告知目標線程,有人但願你退出了。而何時退出,徹底由線程本身自行決定,避免了stop 的問題。可是該方法只是設置標記,因此須要本身判斷狀態而後跳出循環之類的結束線程運行。

那麼咱們怎麼使用這三個方法進行併發編程呢?下面樓主寫了一個例子:

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
      for (; ; ) {
      }
    });
    t1.start();
    Thread.sleep(2000);
    // 不會起任何做用,因此須要判斷他的中斷位狀態
    t1.interrupt();
  }
複製代碼

該測試方法在死循環了一個線程,而後啓動 interrupt 方法,根本不會起任何做用,因此各位不要這樣使用該方法。那麼如何使用呢?示例代碼以下:

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
      for (; ; ) {
        if (Thread.currentThread().isInterrupted()) {
          System.out.println("interrupt");
          break;
        }
        Thread.yield();
      }
    });
    t1.start();
    Thread.sleep(2000);
    t1.interrupt();
  }
複製代碼

使用 isInterrupted 方法進行判斷,若是返回 ture ,表示有中斷標記,那麼則 break 循環。結束運行。

還有一個須要注意的地方就是,若是線程在 sleep 或者 wait 狀態,若是你調用 interrput 方法就會致使InterruptedException 異常,可是,拋出異常時會清除中斷標記,所以,線程也就中斷不了了,若是你想在異常後仍然中斷線程,那麼你須要在 catch 中 繼續設置狀態,也就是調用 interrupt 方法。咱們來個例子看看:

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
      for (; ; ) {
        if (Thread.currentThread().isInterrupted()) {
          System.out.println("interrupt");
          break;
        }
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e) {
          System.err.println("Interrupt When Sleep");
          // 因爲在 sleep 之間中斷線程致使拋出異常,此時,他會清楚中斷位,因此須要在這裏從新設置中斷位,下次循環則會直接判斷中斷標記,從而break。
          Thread.currentThread().interrupt();
          // 該方法會清除中斷狀態,致使上面的一行代碼失效
// boolean isInterrupt = Thread.interrupted();
// System.out.println(isInterrupt);
        }
        Thread.yield();
      }
    });

    t1.start();
    Thread.sleep(1000);
    t1.interrupt();
  }
複製代碼

運行結果:

interrupt Interrupt When Sleep

該測試方法中,在線程中調用了 sleep 方法,並在 main 線程中調用了 interrupt 方法,所以致使該線程異常,可是,若是咱們不在 catch 中從新設置中斷位,該線程永遠不會中止。這個時須要注意的。

還有一個靜態方法,Thread.interrupted(),其實咱們上面的例子也測試了,該方法會返回線程是否中斷,而且會清除狀態,使用的時候須要注意。

4. 經常使用方法2------等待線程結束 join 方法

JDK 源碼:

join 方法

該方法註釋寫到:等待該線程直到死。。。。還真是癡情啊。說正經的的。該方法實際上時等待線程結束。說明意思呢?

假如你有2個線程,A線程在算 1+1 ,而B線程須要 A線程算出的結果,那麼B線程就須要等待A線程,那麼這時候,B線程就須要調用 A線程的 join 方法,調用該方法後, B線程就會被掛起,直到A線程死亡,B線程纔會被喚醒。實際上,若是看 join 的源碼,會發現內部調用了A線程的 wait 方法。也就是說,B 線程 wait 在了 A 線程上。A 線程執行完畢會調用 notifyAll 方法,喚醒B線程。

咱們寫個demo:

package cn.think.in.java.two;

public class JoinTest {

  static int i;

  public static void main(String[] args) throws InterruptedException {
    AddThread addThread = new AddThread();
    addThread.start();
    // 主函數等待 addThread
    // join 的本質是調用了 wait方法,讓調用線程 wait 在當前線程對象實例上。也就是main線程 wait 在 addThread 線程實例上。
    // 當 addThread 執行結束後,會調用 notifyAll 方法,注意,不要再程序中調用線程的 wait 或者 notify 方法,
    // 可能會影響系統API 的工做。
    addThread.join();// 重載方法 join(long) 若是達到給定的毫秒數,則不等了
    System.out.println(i);
  }


  static class AddThread extends Thread {

    public void run() {
      for (; i < 10000000; i++) {
      }
    }
  }

}

複製代碼

該測試方法運行了一個對變量 i 自增運算的線程,而且主線程在等待 addThread 線程執行完纔打印 i 的結果。若是不使用 join , 那麼 ,打印 i 的值永遠會小於10000。

而 join 的內部實現,咱們剛剛說了,使用 wait 方法,咱們看看該方法:

public final synchronized void join(long millis) throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }
複製代碼

該方法時同步的,同時內部調用了自身的 wait 方法。注意:咱們最好不要調用線程的 wait 方法和 notify 方法,可能會致使系統 api 出現問題。

5. 經常使用方法3------線程讓出時間片 yield 方法

這個方法就比較簡單了。這是一個靜態方法,yield 謙讓出CPU時間片;

yieid 會讓出時間片,可是是隨機的。若是你以爲一個線程不是很重要,那就能夠適當的調用該方法,給予其餘線程更多的機會。

拾遺

sleep 方法

雖然用的不少,但有必要說一下,該方法不會釋放當前線程的鎖。面試中常有該問題,wait 方法和 sleep 方法有什麼不一樣,wait 方法會釋放鎖,sleep 方法不會釋放鎖。

holdsLock 方法:

僅噹噹前線程在指定的對象上保持監視器鎖時,才返回  true。該方法旨在使程序可以斷言當前線程已經保持一個指定的鎖。 參數: obj - 用於測試鎖所屬權的對象 返回: 若是當前線程在指定的對象上保持監視器鎖,則返回 true。

setContextClassLoader 方法:

設置該線程的上下文 ClassLoader。上下文 ClassLoader 能夠在建立線程設置,並容許建立者在加載類和資源時向該線程中運行的代碼提供適當的類加載器。 首先,若是有安全管理器,則經過 RuntimePermission("setContextClassLoader") 權限調用其 checkPermission`方法,查看是否能夠設置上下文 ClassLoader。該方法在違反 JDK 默認的類加載模型時能起到很大做用。

參數: 該線程的上下文 ClassLoader

拋出: SecurityException - 若是當前線程沒法設置上下文 ClassLoader。

相關文章
相關標籤/搜索