程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> 編寫高質量代碼:改善Java程序的151個建議(第8章:多線程和並發___建議118~122),151118

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

編輯:JAVA綜合教程

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


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

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

  多線程比較簡單的實現方式是繼承Thread類,然後覆寫run方法,在客戶端程序中通過調用對象的start方法即可啟動一個線程,這是多線程程序的標准寫法。不知道大家能夠還能回想起自己寫的第一個多線程的demo呢?估計一般是這樣寫的:

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比較聰明的地方,簡化多線程應用。

  那可能有人要問了:如果確實有必要覆寫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塊中。這段代碼的執行順序如下:

  分析了這麼多,相信大家也明白了輸出結果,結果如下:

    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),但也是不能完全保證。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved