編寫高質量代碼:改善Java程序的151個建議(第8章:多線程和併發___建議118~121)

  多線程技術能夠更好地利用系統資源,減小用戶的響應時間,提升系統的性能和效率,但同時也增長了系統的複雜性和運維難度,特別是在高併發、大壓力、高可靠性的項目中。線程資源的同步、搶佔、互斥都須要慎重考慮,以免產生性能損耗和線程死鎖。數據庫

建議118:不推薦覆寫start方法

  多線程比較簡單的實現方式是繼承Thread類,而後覆寫run方法,在客戶端程序中經過調用對象的start方法便可啓動一個線程,這是多線程程序的標準寫法。不知道你們可以還能回想起本身寫的第一個多線程的demo呢?估計通常是這樣寫的:windows

class MultiThread extends Thread{
    @Override
    public synchronized void start() {
        //調用線程體
run();
} @Override public void run() { //MultiThread do someThing } }

覆寫run方法,這好辦,寫上本身的業務邏輯便可,但爲何要覆寫start方法呢?最多見的理由是:要在客戶端調用start方法啓動線程,不覆寫start方法怎麼啓動run方法呢?因而乎就覆寫了start方法,在方法內調用run方法。客戶端代碼是一個標準程序,代碼以下 安全

public static void main(String[] args) {
        //多線程對象
        MultiThread m = new MultiThread();
        //啓動多線程
        m.start();
    }

  相信你們都能看出,這是一個錯誤的多線程應用,main方法根本就沒有啓動一個子線程,整個應用程序中只有一個主線程在運行,並不會建立任何其它的線程。對此,有很簡單的解決辦法。只要刪除MultiThread類的start方法便可。多線程

  而後呢?就結束了嗎?是的,不少時候確實到此結束了。那爲何沒必要並且不能覆寫start方法,僅僅就是由於" 多線程應用就是這樣寫的 " 這個緣由嗎?併發

  要說明這個問題,就須要看一下Thread類的源碼了。Thread類的start方法的代碼(這個是JDK7版本的)以下: 運維

public synchronized void start() {
        // 判斷線程狀態,必須是爲啓動狀態
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
        // 加入線程組中
        /*
         * Notify the group that this thread is about to be started so that it
         * can be added to the group's list of threads and the group's unstarted
         * count can be decremented.
         */
        group.add(this);
        boolean started = false;
        try {
            // 分配棧內存,啓動線程,運行run方法
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /*
                 * do nothing. If start0 threw a Throwable then it will be
                 * passed up the call stack
                 */
            }
        }
    }
   // 本地方法
private native void start0();

  這裏的關鍵是本地方法start0,它實現了啓動線程、申請棧內存、運行run方法、修改線程狀態等職責,線程管理和棧內存管理都是由JVM負責的,若是覆蓋了start方法,也就是撤銷了線程管理和棧內存管理的能力,這樣如何啓動一個線程呢?事實上,不須要關注線程和棧內存的管理,主須要編碼者實現多線程的邏輯便可(即run方法體),這也是JVM比較聰明的地方,簡化多線程應用。ide

  那可能有人要問了:若是確實有必要覆寫start方法,那該如何處理呢?這確實是一個罕見的要求,不過覆寫也容易,只要在start方法中加上super.start()便可,代碼以下:高併發

class MultiThread extends Thread {
    @Override
    public synchronized void start() {
        /* 線程啓動前的業務處理 */
        super.start();
        /* 線程啓動後的業務處理 */
    }

    @Override
    public void run() {
        // MultiThread do someThing
    }

}

  注意看start方法,調用了父類的start方法,沒有主動調用run方法,這是由JVM自行調用的,不用咱們顯示實現,並且是必定不能實現。此方式雖然解決了" 覆寫start方法 "的問題,可是基本上無用武之地,到目前爲止尚未發現必定要覆寫start方法的多線程應用,因此要求覆寫start的場景。均可以使用其餘的方式實現,例如類變量、事件機制、監聽等方式。性能

注意:繼承自Thread類的多線程類沒必要覆寫start方法。優化

建議119:啓動線程前stop方法是不可靠的

  有這樣一個案例,咱們須要一個高效率的垃圾郵件製造機,也就是有儘量多的線程來儘量多的製造垃圾郵件,垃圾郵件重要的信息保存在數據庫中,如收件地址、混淆後的標題、反應垃圾處理後的內容等,垃圾製造機的做用就是從數據庫中讀取這些信息,判斷是否符合條件(如收件地址必須包含@符號、標題不能爲空等),而後轉換成一份真實的郵件發出去。

  整個應用邏輯很簡單,這必然是一個多線程應用,垃圾郵件製造機須要繼承Thread類,代碼以下:

//垃圾郵件製造機
class SpamMachine extends Thread{
    @Override
    public void run() {
        //製造垃圾郵件
        System.out.println("製造大量垃圾郵件......");
    }
}

  在客戶端代碼中須要發揮計算機的最大潛能來製造郵件,也就是說開儘量多的線程,這裏咱們使用一個while循環來處理,代碼以下:

public static void main(String[] args) {
        //不分晝夜的製造垃圾郵件
        while(true){
            //多線程多個垃圾郵件製造機
            SpamMachine sm = new SpamMachine();
            //xx條件判斷,不符合提交就設置該線程不可執行
            if(!false){
                sm.stop();
            }
            //若是線程是stop狀態,則不會啓動
            sm.start();
        }
    }

  在此段代碼中,設置了一個極端條件:全部的線程在啓動前都執行stop方法,雖然它是一個過期的方法,但它的運行邏輯仍是正常的,何況stop方法在此處的目的並非中止一個線程,而是設置線程爲不可啓用狀態。想來這應該是沒有問題的,可是運行結果卻出現了奇怪的現象:部分線程仍是啓動了,也就是在某些線程(沒有規律)中的start方法正常執行了。在不符合判斷規則的狀況下,不可啓用狀態的線程仍是啓用了。這是爲何呢?

  這是線程啓動start方法的一個缺陷。Thread類的stop方法會根據線程狀態來判斷是終結線程仍是設置線程爲不可運行狀態,對於未啓動的線程(線程狀態爲NEW)來講,會設置其標誌位爲不可啓動,而其餘的狀態則是直接中止。stop方法的JDK1.6源代碼(JDk1.6以上源碼於此可能有變化,須要從新觀察源碼)以下:  

   @Deprecated
    public final void stop() {
        // If the thread is already dead, return.
    // A zero status value corresponds to "NEW".
    if ((threadStatus != 0) && !isAlive()) {
        return;
    }
    stop1(new ThreadDeath());
    }
 private final synchronized void stop1(Throwable th) {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        checkAccess();
        if ((this != Thread.currentThread()) ||
        (!(th instanceof ThreadDeath))) {
        security.checkPermission(SecurityConstants.STOP_THREAD_PERMISSION);
        }
    }
        // A zero status value corresponds to "NEW"
    if (threadStatus != 0) {
        resume(); // Wake up thread if it was suspended; no-op otherwise
        stop0(th);
    } else {

            // Must do the null arg check that the VM would do with stop0
        if (th == null) {
         throw new NullPointerException();
        }

            // Remember this stop attempt for if/when start is used
        stopBeforeStart = true;
        throwableFromStop = th;
        }
    }

  這裏設置了stopBeforeStart變量,標誌着是在啓動前設置了中止標誌,在start方法中(JDK6源碼)是這樣校驗的:  

public synchronized void start() {
        /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added 
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
        group.add(this);
        start0();
// 在啓動前設置了中止狀態
if (stopBeforeStart) { stop0(throwableFromStop); } } private native void start0();

  注意看start0方法和stop0方法的順序,start0方法在前,也就說既是stopBeforeStart爲true(不可啓動),也會啓動一個線程,而後再stop0結束這個線程,而罪魁禍首就在這裏!

  明白了緣由,咱們的情景代碼就很容易修改了,代碼以下:

public static void main(String[] args) {
        // 不分晝夜的製造垃圾郵件
        while (true) {
            // 多線程多個垃圾郵件製造機
            SpamMachine sm = new SpamMachine();
            // xx條件判斷,不符合提交就設置該線程不可執行
            if (!false) {
                new SpamMachine().start();
            }
        }
    }

  再也不使用stop方法進行狀態的設置,直接經過判斷條件來決定線程是否可啓用。對於start方法的缺陷,通常不會引發太大的問題,只是增長了線程啓動和中止的精度而已。

建議120:不使用stop方法中止線程

  線程啓動完畢後,在運行時可能須要停止,Java提供的終止方法只有一個stop,可是我不建議使用這個方法,由於它有如下三個問題:

(1)、stop方法是過期的:從Java編碼規則來講,已通過時的方法不建議採用。

(2)、stop方法會致使代碼邏輯不完整:stop方法是一種" 惡意 " 的中斷,一旦執行stop方法,即終止當前正在運行的線程,無論線程邏輯是否完整,這是很是危險的。看以下的代碼:

public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                try {
                    // 子線程休眠1秒
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // 異常處理
                }
                System.out.println("此處是業務邏輯,永遠不會執行");
            }
        };
        // 啓動線程
        thread.start();
        // 主線程休眠0.1秒
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 子線程中止
        thread.stop();
    }

  這段代碼的邏輯是這樣的:子線程是一個匿名內部類,它的run方法在執行時會休眠一秒,而後執行後續的邏輯,而主線程則是休眠0.1秒後終止子線程的運行,也就說JVM在執行tread.stop()時,子線程還在執行sleep(1000),此時stop方法會清除棧內信息,結束該線程,這也就致使了run方法的邏輯不完整,輸出語句println表明的是一段邏輯,可能很是重要,好比子線程的主邏輯、資源回收、情景初始化等,可是由於stop線程了,這些都再也不執行,因而就產生了業務邏輯不完整的狀況。

  這是極度危險的,由於咱們不知道子線程會在何時被終止,stop連基本的邏輯完整性都沒法保證。並且此種操做也是很是隱蔽的,子線程執行到何處會被關閉很難定位,這位之後的維護帶來了不少麻煩。

(3)、stop方法會破壞原子邏輯

  多線程爲了解決共享資源搶佔的問題,使用了鎖概念,避免資源不一樣步,可是正由於此,stop方法卻會帶來更大的麻煩,它會丟棄全部的鎖,致使原子邏輯受損。例若有這樣一段程序:

class MultiThread implements Runnable {
    int a = 0;
    @Override
    public void run() {
        // 同步代碼塊,保證原子操做
        synchronized ("") {
            // 自增
            a++;
            try {
                //線程休眠0.1秒
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 自減
            a--;
            String tn = Thread.currentThread().getName();
            System.out.println(tn + ":a = " + a);
        }
    }

}

  MultiThread實現了Runnable接口,具有多線程能力,其中run方法中加上了synchronized代碼塊,表示內部是原子邏輯,它會先自增而後自減,按照synchronized同步代碼塊的規則來處理,此時不管啓動多少線程,打印出來的結果應該是a=0,可是若是有一個正在執行的線程被stop,就會破壞這種原子邏輯,代碼以下:  

    public static void main(String[] args) {
        MultiThread t = new MultiThread();
        Thread t1 = new Thread(t);
        // 啓動t1線程
        t1.start();
        for (int i = 0; i < 5; i++) {
            new Thread(t).start();
        }
        //中止t1線程
        t1.stop();
    }

  首先說明的是全部線程共享了一個MultiThread的實例變量t,其次因爲在run方法中加入了同步代碼塊,因此只能有一個線程進入到synchronized塊中。這段代碼的執行順序以下:

  1. 線程t1啓動,並執行run方法,因爲沒有其它線程同步代碼塊的鎖,因此t1線程執行後自加後執行到sleep方法即開始休眠,此時a=1
  2. JVM又啓動了5個線程,也同時運行run方法,因爲synchronized關鍵字的阻塞做用,這5個線程不能執行自增和自減操做,等待t1線程鎖釋放。
  3. 主線程執行了t1.stop方法,終止了t1線程,注意,因爲a變量是全部線程共享的,因此其它5個線程得到的a變量也是1
  4. 其它5個線程依次得到CPU執行機會,打印出a值

  分析了這麼多,相信你們也明白了輸出結果,結果以下:

    Thread-5:a = 1
    Thread-4:a = 1
    Thread-3:a = 1
    Thread-2:a = 1
    Thread-1:a = 1

  本來指望synchronized同步代碼塊中的邏輯都是原子邏輯,不受外界線程的干擾,可是結果卻出現原子邏輯被破壞的狀況,這也是stop方法被廢棄的一個重要緣由:破壞了原子邏輯。

  既然終止一個線程不能使用stop方法,那怎樣才能終止一個正在運行的線程呢?答案也簡單,使用自定義的標誌位決定線程的執行狀況,代碼以下:

class SafeStopThread extends Thread {
    // 此變量必須加上volatile
    /*
     * volatile: 1.做爲指令關鍵字,確保本條指令不會因編譯器的優化而省略,且要求每次直接讀值.
     * 2.被設計用來修飾被不一樣線程訪問和修改的變量。若是不加入volatile
     * ,基本上會致使這樣的結果:要麼沒法編寫多線程程序,要麼編譯器失去大量優化的機會。
     */
    private volatile boolean stop = false;

    @Override
    public void run() {
        // 判斷線程體是否運行
        while (stop) {
            // doSomething
        }
    }

    public void terminate() {
        stop = true;
    }
}

  這是很簡單的辦法,在線程體中判斷是否須要中止運行,便可保證線程體的邏輯完整性,並且也不會破壞原子邏輯。可能你們對JavaAPI比較熟悉,因而提出疑問:Thread不是還提供了interrupt中斷線程的方法嗎?這個方法可不是過期方法,那可使用嗎?它能夠終止一個線程嗎?

  interrupt,名字看上去很像是終止一個線程的方法,但它不能終止一個正在執行着的線程,它只是修改中斷標誌而已,例以下面一段代碼:

    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                // 線程一直運行
                while (true) {
                    System.out.println("Running......");
                }
            }
        };
        // 啓動線程
        thread.start();
        // 中斷線程
        thread.interrupt();
    }

  執行這段代碼,你會發現一直有Running在輸出,永遠不會中止,彷佛執行了interrupt沒有任何變化,那是由於interrupt方法不能終止一個線程狀態,它只會改變中斷標誌位(若是在thread.interrupt()先後輸出thread.isInterrupted()則會發現分別輸出了false和true),若是須要終止該線程,還須要本身進行判斷,例如咱們可使用interrupt編寫出更簡潔、安全的終止線程代碼:

class SafeStopThread extends Thread {
    @Override
    public void run() {
        //判斷線程體是否運行
        while (!isInterrupted()) {
            // do SomeThing
        }
    }
}

   總之,若是指望終止一個正在運行的線程,則不能使用已過期的stop方法。須要自行編碼實現,如此便可保證原子邏輯不被破壞,代碼邏輯不會出現異常。固然,若是咱們使用的是線程池(好比ThreadPoolExecutor類),那麼能夠經過shutdown方法逐步關閉池中的線程,它採用的是比較溫和、安全的關閉線程方法,徹底不會產生相似stop方法的弊端。

建議121:線程優先級只使用三個等級

  線程的優先級(Priority)決定了線程得到CPU運行的機會,優先級越高得到的運行機會越大,優先級越低得到的機會越小。Java的線程有10個級別(準確的說是11個級別,級別爲0的線程是JVM的,應用程序不能設置該級別),那是否是說級別是10的線程確定比級別是9的線程先運行呢?咱們來看以下一個多線程類:

class TestThread implements Runnable {
    public void start(int _priority) {
        Thread t = new Thread(this);
        // 設置優先級別
        t.setPriority(_priority);
        t.start();
    }
    @Override
    public void run() {
        // 消耗CPU的計算
        for (int i = 0; i < 100000; i++) {
            Math.hypot(924526789, Math.cos(i));
        }
        // 輸出線程優先級
        System.out.println("Priority:" + Thread.currentThread().getPriority());
    }
}

  該多線程實現了Runnable接口,實現了run方法,注意在run方法中有一個比較佔用CPU的計算,該計算毫無心義,

public static void main(String[] args) {
        //啓動20個不一樣優先級的線程
        for (int i = 0; i < 20; i++) {
            new TestThread().start(i % 10 + 1);
        }
    }

 這裏建立了20個線程,每一個線程在運行時都耗盡了CPU的資源,由於優先級不一樣,線程調度應該是先處理優先級高的,而後處理優先級低的,也就是先執行2個優先級爲10的線程,而後執行2個優先級爲9的線程,2個優先級爲8的線程......可是結果卻並非這樣的。

  Priority:5
  Priority:7
  Priority:10
  Priority:6
  Priority:9
  Priority:6
  Priority:5
  Priority:7
  Priority:10
  Priority:3
  Priority:4
  Priority:8
  Priority:8
  Priority:9
  Priority:4
  Priority:1
  Priority:3
  Priority:1
  Priority:2
  Priority:2

  println方法雖然有輸出損耗,可能會影響到輸出結果,可是無論運行多少次,你都會發現兩個不爭的事實:

(1)、並非嚴格按照線程優先級來執行的

  好比線程優先級爲5的線程比優先級爲7的線程先執行,優先級爲1的線程比優先級爲2的線程先執行,不多出現優先級爲2的線程比優先級爲10的線程先執行(注意,這裏是" 不多 ",是說確實有可能出現,只是概率低,由於優先級只是表示線程得到CPU運行的機會,並不表明強制的排序號)。

(2)、優先級差異越大,運行機會差異越明顯

  好比優先級爲10的線程一般會比優先級爲2的線程先執行,可是優先級爲6的線程和優先級爲5的線程差異就不太明顯了,執行屢次,你會發現有不一樣的順序。

  這兩個現象是線程優先級的一個重要表現,之因此會出現這種狀況,是由於線程運行是須要得到CPU資源的,那誰能決定哪一個線程先得到哪一個線程後得到呢?這是依照操做系統設置的線程優先級來分配的,也就是說,每一個線程要運行,須要操做系統分配優先級和CPU資源,對於JAVA來講,JVM調用操做系統的接口設置優先級,好比windows操做系統優先級都相同嗎?

  事實上,不一樣的操做系統線程優先級是不一樣的,Windows有7個優先級,Linux有140個優先級,Freebsd則由255個(此處指的優先級個數,不一樣操做系統有不一樣的分類,如中斷級線程,操做系統級等,各個操做系統具體用戶可用的線程數量也不相同)。Java是跨平臺的系統,須要把這10個優先級映射成不一樣的操做系統的優先級,因而界定了Java的優先級只是表明搶佔CPU的機會大小,優先級越高,搶佔CPU的機會越大,被優先執行的可能性越高,優先級相差不大,則搶佔CPU的機會差異也不大,這就是致使了優先級爲9的線程可能比優先級爲10的線程先運行。

  Java的締造者們也覺察到了線程優先問題,因而Thread類中設置了三個優先級,此意就是告訴開發者,建議使用優先級常量,而不是1到10的隨機數字。常量代碼以下: 

public class Thread implements Runnable {
    /**
     * The minimum priority that a thread can have. 
     */
    public final static int MIN_PRIORITY = 1;
    /**
     * The default priority that is assigned to a thread. 
     */
    public final static int NORM_PRIORITY = 5;
    /**
     * The maximum priority that a thread can have. 
     */
    public final static int MAX_PRIORITY = 10;


}

  在編碼時直接使用這些優先級常量,能夠說在大部分狀況下MAX_PRIORITY的線程回比MIN_PRIORITY的線程優先運行,可是不能認爲是必然會先運行,不能把這個優先級作爲核心業務的必然條件,Java沒法保證優先級高確定會先執行,只能保證高優先級有更多的執行機會。所以,建議在開發時只使用此三類優先級,沒有必要使用其餘7個數字,這樣也能夠保證在不一樣的操做系統上優先級的表現基本相同。

  你們也許會問,若是優先級相同呢?這很好辦,也是由操做系統決定的。基本上是按照FIFO原則(先入先出,First Input First Output),但也是不能徹底保證。

相關文章
相關標籤/搜索