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

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

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

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

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

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

建議122:使用線程異常處理器提高系統可靠性運維

建議123:volatile不能保證數據同步異步

建議124:異步運算考慮使用Callable接口socket

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

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

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

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

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

三、stop方法破壞原子邏輯

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

如何關閉線程呢?

if (!thread.isInterrupted()) {
    thread.interrupt();
}

若是使用的是線程池,能夠經過shutdown方法逐步關閉池中的線程。

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

線程的優先級(Priority)決定了線程獲取CPU運行的機會,優先級越高獲取的運行機會越大,優先級月底獲取的機會越小。

package OSChina.Multithread;

public 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());
    }

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

建立了20個線程,優先級設置的不一樣,執行起來是這樣的,5和6反了。

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

由於優先級只是表示線程獲取CPU運行的機會,並非代碼強制的排序號。

二、優先級差異越大,運行機會差異越明顯

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

public class Thread implements Runnable {
    public final static int MIN_PRIORITY = 1;
    public final static int NORM_PRIORITY = 5;
    public final static int MAX_PRIORITY = 10;
}

開發時只使用此三類優先級就能夠了。

建議122:使用線程異常處理器提高系統可靠性

編寫一個socket應用,監聽指定端口,實現數據包的接收和發送邏輯,這在早起系統間進行數據交互是常用的,這類接口一般考慮兩個問題:一個是避免線程阻塞,保證接收的數據儘快處理;二是接口的穩定性和可靠性,數據包很複雜,接口服務的系統也不少,一旦守候線程出現異常就會致使socket中止,這是很是危險的,那咱們有什麼辦法避免呢?

Java1.5版本之後在thread類中增長了setUncaughtExceptionHandler方法,實現了線程異常的捕捉和處理。

代碼實例:

package OSChina.Multithread;

public class TcpServer implements Runnable {
    public TcpServer() {
        Thread t = new Thread(this);
        t.setUncaughtExceptionHandler(new TcpServerExceptionHandler());
        t.start();
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            try{
                Thread.sleep(1000);
                System.out.println("系統正常運行:"+i);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
        throw new RuntimeException();
    }

    private static class TcpServerExceptionHandler implements Thread.UncaughtExceptionHandler{
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            System.out.println("線程"+t.getName()+" 出現異常,自行重啓,請分析緣由。");
            e.printStackTrace();
            new TcpServer();
        }
    }

    public static void main(String[] args) {
        TcpServer tcpServer = new TcpServer();
    }
}

這段代碼的邏輯比較簡單,在TcpServer類建立時啓動一個線程,提供TCP服務,例如接收和發送文件,具體邏輯在run方法中實現。同時,設置了該線程出現運行期異常時,由TcpServerExceptionHandler異常處理器來處理異常。那麼TcpServerExceptionHandler作什麼呢?兩件事:

一、記錄異常信息,以便查找問題

二、從新啓動一個新線程,提供不間斷的服務

有了這兩點,TcpServer就能夠穩定的運行了,即便出現異常也能自動重啓,客戶代碼比較簡單,只須要new TcpServer()便可,運行結果以下:

從運行結果能夠看出,當Thread-0出現異常時,系統自動重啓了Thread-1線程,繼續提供服務,大大提升了系統的性能。

這段代碼只是一個示例程序,若要在實際環境中應用,則須要注意如下三個方面:

一、共享資源鎖定:若是線程產生異常的緣由是資源被鎖定,自動重啓應用會增長系統的負擔,沒法提供不間斷服務。例如一個即時通訊服務出現信息不能寫入的狀況,即時再怎麼重啓服務,也沒法解決問題。在此狀況下最好的辦法是中止全部的線程,釋放資源。

二、髒數據引發系統邏輯混亂:異常的產生中斷了正在執行的業務邏輯,特別是若是正在處理一個原子操做,但若是此時拋出了運行期異常就有可能會破壞正常的業務邏輯,例如出現用戶認證經過了,但簽到不成功的狀況,在這種狀況下重啓應用程序,雖然能夠提供服務,但對部分用戶產生了邏輯異常。

三、內存溢出:線程異常了,但由該線程建立的對象並不會立刻回收,若是再從新啓動新線程,再建立一批對象,特別是加入了場景接管,就很是危險了,例如即時通訊服務,從新啓動一個新線程必須保證原在線用戶的透明性,即用戶不會察覺服務重啓,在這種狀況下,就須要在線程初始化時加載大量對象以保證用戶的狀態信息,可是若是線程反覆重啓,極可能會引發OutOfMemory內存泄漏問題。

建議123:volatile不能保證數據同步

volatile關鍵字比較少用,緣由無外乎兩點,一是在Java1.5以前該關鍵字在不一樣的操做系統上有不一樣的表現,所帶來的問題就是移植性較差;並且比較難設計,誤用較多,這也致使它的「名譽」受損。

咱們知道,每一個線程都運行在棧內存中,每一個線程都有本身的工做內存(Working Memory,好比寄存器Register、高速緩存存儲器Cache等),線程的計算通常是經過工做內存進行交互的,其示意圖以下圖所示:

從示意圖中咱們能夠看到,線程在初始化時從主內存中加載須要的變量值到工做內存中,而後在線程運行時,若是是讀取,直接從工做內存中讀取,若是是寫入,則先寫入工做內存中,以後刷新到主內存中,這是JVM的一個簡單的內存模型,可是這樣的結構在多線程的狀況下有可能會出現問題,好比:A線程修改變量的值,也刷新到了主內存,但B、C線程在此時間內讀取的仍是本線程的工做內存,也就是說它們讀取的不是最新的值,此時就會出現不一樣線程持有的公共資源不一樣步的狀況。

對於此問題有不少解決的辦法,好比使用synchronized同步代碼塊,或者使用Lock鎖來解決該問題,不過,Java可使用volatile更簡單的解決此類問題,好比在一個變量前加上volatile關鍵字,能夠確保每一個線程對本地變量的訪問和修改都是直接與內存交互的,而不是與本線程的工做內存交互的,保證每一個線程都能獲取到最新的變量值,其示意圖以下:

明白了volatile變量的原理,那咱們來思考一下:volatile變量是否可以保證數據的同步性呢?兩個線程同時修改volatile變量是否會產生髒數據呢?代碼以下:

package OSChina.Multithread;

public class UnsafeThread implements Runnable {
    //共享資源
    private volatile int count = 0;
    @Override
    public void run() {
        // 增長CPU的繁忙程度,沒必要關心其邏輯含義
        for (int i = 0; i < 1000; i++) {
            Math.hypot(Math.pow(92456789,i),Math.cos(i));
        }
        count++;
    }
    public int getCount(){
        return count;
    }
}

上面的代碼定義了一個多線程,run方法的主要邏輯是共享資源count的自加運算,並且咱們還爲count變量加上了volatile關鍵字,確保是從內存中讀取和寫入的,若是有多個線程運行,也就是多個線程執行count變量的自加操做,count變量會產生髒數據嗎?模擬多線程代碼以下:

public static void main(String[] args) {
        // 理想值,並做爲最大循環次數
        int value = 1000;
        // 循環次數,防止形成無限循環或者死循環
        int loops = 0;
        // 主線程組,用於估計活動線程數
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        while (loops++<value){
            // 共享資源清零
            UnsafeThread ut = new UnsafeThread();
            for (int i = 0; i < value; i++) {
                new Thread(ut).start();
            }
            // 先等15毫秒,等待活動線程爲1
            do {
                try {
                    Thread.sleep(15);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }while (tg.activeCount()!=1);
            //檢查實際值與理論值是否一致
            if(ut.getCount()!=value){
                //出現線程不安全的狀況
                System.out.println("循環到:"+loops+" 遍,出現線程不安全的狀況");
                System.out.println("此時,count= "+ut.getCount());
                System.exit(0);
            }
        }
    }

此段代碼的邏輯以下:

一、啓動1000個線程,修改共享資源count的值

二、暫停15毫秒,觀察活動線程數是否爲1(即只剩下主線程再運行),若不爲1,則再等待15毫秒。

三、共享資源是不是不安全的,即實際值與理想值是否相同,若不相同,則發現目標,此時count的值爲髒數據。

四、若是沒有找到,繼續循環,直到達到最大循環爲止。

運行結果:

執行完了,沒出現不安全的狀況,證實volatile性能仍是能夠的。

書中自有黃金屋,書中自有顏如玉!

書中的運行結果:

循環到:40遍,出現不安全的狀況

此時,count=999

這只是一種可能的結果,每次執行都有可能產生不一樣的結果。這也說明咱們的count變量沒有實現數據同步,在多個線程修改的狀況下,count的實際值與理論值產生了誤差,直接說明了volatile關鍵字並不能保證線程的安全。

代碼執行完畢,本來指望的結果爲1000,但運行後的結果爲999,這表示出現了線程不安全的狀況。這也就說明了:volatile關鍵字只能保證當前線程須要該變量的值時可以得到最新的值,並不能保證線程修改的安全性。

順便說一下,上面的代碼中,UnsafeThread類消耗CPU計算時必須的,其目的是加劇線程的負荷,以便出現單個線程搶佔整個CPU資源的情景,否者很難模擬出volatile線程不安全的狀況,你們能夠實際測試一下。

UnsafeThread消耗CPU很嚴重,慎用啊。

建議124:異步運算考慮使用Callable接口

多線程應用有兩種實現方式,一種是實現runnable接口,另外一種是繼承Thread類,這兩種方法都有缺點:run方法沒有返回值,不能拋出異常(這兩個缺點歸根到底就是runnable接口的缺陷,Thread類也是實現了runnable接口),若是須要知道一個線程的運行結果就須要用戶自行設計,線程類自己並不能提供返回值和異常。可是Java1.5引入了一個新的接口callable,它相似於runnable接口,實現它也能夠實現多線程任務。

好很差測一下:

package OSChina.Multithread;

import java.util.concurrent.*;

public class TaxCalculator implements Callable {
    //本金
    private int seedMoney;

    //接收主線程提供的參數
    public TaxCalculator(int _seedMoney){
        seedMoney = _seedMoney;
    }

    @Override
    public Integer call() throws Exception {
        // 複雜計算,運行一次須要2秒
        TimeUnit.MILLISECONDS.sleep(2000);
        return seedMoney/10;
    }
}

模擬一個複雜運算:稅款計算器,該運算可能要花費10秒的時間,用戶此時一直等啊等,很煩躁,須要給點提示,讓用戶知道程序在運行,沒卡死。

public static void main(String[] args) throws InterruptedException, ExecutionException {
        //生成一個單線程的異步執行器
        ExecutorService es = Executors.newSingleThreadExecutor();
        //線程執行後的指望值
        Future<Integer> future = es.submit(new TaxCalculator(100));
        while (!future.isDone()){
            // 尚未運算完成,等待50毫秒
            TimeUnit.MILLISECONDS.sleep(50);
            System.out.print("*");
        }
        System.out.println("\n計算完成,稅金是:"+future.get()+" 元");
        es.shutdown();
    }

Executors是一個靜態工具類,提供了異步執行器的建立能力,如單線程異步執行器newSingleThreadExecutor、固定線程數量的執行器newFixedThreadPool等,通常它是異步計算的入口類。future關注的是線程執行後的結果,好比運行十分完畢,結果是多少等。

執行時,"*"會依次遞增,表示系統正在運算,爲用戶提供了運算進度,此類異步計算的好處是:

一、儘量多的佔用系統資源,提升運算速度

二、能夠監控線程的執行狀況。好比執行是否完畢、是否有返回值、是否有異常等。

三、能夠爲用戶提供更好的支持,好比例子中的運算進度等。

 

編寫高質量代碼:改善Java程序的151個建議@目錄

相關文章
相關標籤/搜索