java安全編碼指南之:Thread API呼叫規則

flydean發表於2020-10-19

簡介

java中多執行緒的開發中少不了使用Thread,我們在使用Thread中提供的API過程中,應該注意些什麼規則呢?

一起來看一看吧。

start一個Thread

Thread中有兩個方法,一個是start方法,一個是run方法,兩個都可以呼叫,那麼兩個有什麼區別呢?

先看一下start方法:

public synchronized void start() {

        if (threadStatus != 0)
            throw new IllegalThreadStateException();
        group.add(this);
        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
            }
        }
    }

    private native void start0();

start()是一個synchronized的方法,通過它會去呼叫native的start0方法,而最終將會呼叫Thread的run()方法。

我們知道,建立一個Thread有兩種方式,一種是傳入一個Runnable,一個是繼承Thread,並重寫run()方法。

如果我們直接呼叫Thread的run()方法會發生什麼事情呢?

先看一下run方法的定義:

    public void run() {
        if (target != null) {
            target.run();
        }
    }

預設情況下, 這個target就是一個Runnable物件,如果Thread是通過Runnable來構建的話,呼叫Thread.run()會在當前執行緒中執行run方法中的內容。

如果Thread是以其形式構建,並且沒有重新run()方法,那麼直接呼叫Thread.run()將什麼都不會做。

    public void wrongStart(){
        Runnable runnable= ()-> System.out.println("in thread running!");
        Thread thread= new Thread(runnable);
        thread.run();
    }

    public void correctStart(){
        Runnable runnable= ()-> System.out.println("in thread running!");
        Thread thread= new Thread(runnable);
        thread.start();
    }

所以,上面兩種呼叫方式,只有第二種是正確的。

不要使用ThreadGroup

Thread中有個欄位型別是java.lang.ThreadGroup,這個主要是用來給Thread進行分組,我們看下Thread的這個建構函式:

 public Thread(ThreadGroup group, Runnable target) {
        this(group, target, "Thread-" + nextThreadNum(), 0);
    }

上面的建構函式可以在傳入runnable的同時傳遞一個ThreadGroup對Thread進行分組。

如果沒有指定ThreadGroup,那麼將會為其分配一個預設的default group。

ThreadGroup是做什麼的呢?ThreadGroup是java 1.0引入的方法,主要是一次性的對一組thread進行操作。我們可以呼叫ThreadGroup.interrupt()來一次性的對整個Group的Thread進行interrupts操作。

雖然ThreadGroup提供了很多有用的方法,但是其中很多方法都被廢棄了,比如:allowThreadSuspension(), resume(), stop(), 和 suspend(),並且ThreadGroup中還有很多方法是非執行緒安全的:

  • ThreadGroup.activeCount()

這個方法主要是用來統計一個ThreadGroup中活動的執行緒個數,這個方法會統計還未啟動的執行緒,同時也會受系統執行緒的影響,所以是不準確的。

  • ThreadGroup.enumerate()

這個方法是將ThreadGroup和子group的執行緒拷貝到一個陣列中,但是如果陣列太小了,多餘的執行緒是會被自動忽略的。

ThreadGroup本身有一個 stop() 方法用來停止所有的執行緒,但是stop是不安全的,已經被廢棄了。

那麼我們該怎麼去安全的停止很多個執行緒呢?

使用executor.shutdown()就可以了。

不要使用stop()方法

剛剛講了ThreadGroup中不要呼叫stop()方法,因為stop是不安全的。

呼叫stop方法會立馬釋放執行緒持有的所有的鎖,並且會丟擲ThreadDeath異常。

因為會釋放所有的鎖,所以可能會造成受這些鎖保護的物件的狀態發生不一致的情況。

替代的方法有兩種,一種是使用volatile flag變數,來控制執行緒的迴圈執行:

    private volatile boolean done = false;

    public void shutDown(){
        this.done= true;
    }

    public void stopWithFlag(){

        Runnable runnable= ()->{
            while(!done){
                System.out.println("in Runnable");
            }
        };

        Thread thread= new Thread(runnable);
        thread.start();
        shutDown();
    }

另外一種方法就是呼叫interrupt(), 這裡我們要注意interrupt()的使用要點:

  1. 如果當前執行緒例項在呼叫Object類的wait(),wait(long)或wait(long,int)方法或join(),join(long),join(long,int)方法,或者在該例項中呼叫了Thread.sleep(long)或Thread.sleep(long,int)方法,並且正在阻塞狀態中時,則其中斷狀態將被清除,並將收到InterruptedException。

  2. 如果此執行緒在InterruptibleChannel上的I/O操作中處於被阻塞狀態,則該channel將被關閉,該執行緒的中斷狀態將被設定為true,並且該執行緒將收到java.nio.channels.ClosedByInterruptException異常。

  3. 如果此執行緒在java.nio.channels.Selector中處於被被阻塞狀態,則將設定該執行緒的中斷狀態為true,並且它將立即從select操作中返回。

  4. 如果上面的情況都不成立,則設定中斷狀態為true。

先看下面的例子:

    public static void main(String[] args)  {
        Runnable runnable= ()->{
            while (!Thread.interrupted()) {
             System.out.println("in thread");
            }
        };
        Thread thread= new Thread(runnable);
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }

我們在while迴圈中呼叫了Thread.interrupted()方法用來判斷執行緒是否被設定了中斷位,然後在main方法中呼叫了thread.interrupt()來設定中斷,最終可以正確的停止Thread。

注意,這裡執行的Thread並沒有被阻塞,所以並不滿足我們上面提到的第一個條件。

下面我們再看一個例子:

    public static void main(String[] args)  {
        Runnable runnable= ()->{
            while (!Thread.interrupted()) {
             System.out.println("in thread");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread thread= new Thread(runnable);
        thread.start();
        thread.interrupt();
    }

這個例子和上面的例子不同之處就是在於,Thread中呼叫了sleep方法,導致Thread被阻塞了,最終滿足了第一個條件,從而不會設定終端位,只會丟擲InterruptedException,所以這個例子中執行緒是不會被停止的,大家一定要注意。

wait 和 await 需要放在迴圈中呼叫

為什麼要放在迴圈中呢?因為我們希望wait不是被錯誤的被喚醒,所以我們需要在wait被喚醒之後,重新檢測一遍條件。

錯誤的呼叫是放在if語句中:

synchronized (object) {
  if (<condition does not hold>) {
    object.wait();
  }
  // Proceed when condition holds
}

正確的方法是放在while迴圈中:

synchronized (object) {
  while (<condition does not hold>) {
    object.wait();
  }
  // Proceed when condition holds
}

本文的程式碼:

learn-java-base-9-to-20/tree/master/security

本文已收錄於 http://www.flydean.com/java-security-code-line-thread/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!

相關文章