Java多線程(1)——基礎

本章先來介紹一下多線程開發的基礎內容。

1、進程與線程

進程 是什麼,想必學計算機的同學都不會陌生,打開windows任務管理器,或者 linux 服務器上top命令鎖展示的結果,就是一個個的進程。

進程(Process)是計算機中的程序關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操作系統結構的基礎。在早期面向進程設計的計算機結構中,進程是程序的基本執行實體;在當代面向線程設計的計算機結構中,進程是線程的容器。程序是指令、數據及其組織形式的描述,進程是程序的實體。——引自百度百科

但是一個應用只有一個進程嗎,NO,一個應用可能會開除多個子進程來進行需要的 工作 。舉個例子, php 解析的服務進程,可以配置要使用多少個子進程進行工作,所以在top命令中,會看到多個同名進程,而他們的創建者都是php主進程創建的。

線程 大家肯定也聽過,一般一個進程下會有多個線程。

線程,有時被稱爲輕量級進程,是程序執行流的最小單元。在單個程序中同時運行多個線程完成不同的工作,稱爲多線程。——引自百度百科

另外,一個進程至少會有一個線程,如果只有一個線程,那就是程序本身了。一般程序有多個線程的話都會有一個主線程去執行入口程序,之後再發起其他線程去進行其他的工作。

看起來還是不太清晰的話,我來舉個例子解釋一下。比如一個應用,需要幹兩件事情,A是發送一個請求等待響應之後打印出來(這個請求加上響應的時間可能是2s),B也是發送一個請求等待響應之後打印出來(這個時間是3s)。如果沒有線程就需要順序執行了,這個總的執行時間我們可以預估到是5s。如果使用了多線程,線程1發起請求A,這時候再起一個線程2發起請求B,線程1等待2s收到響應打印,線程2等待3s收到響應打印。總的程序執行時間就是3s了。

其實多線程,是利用CPU空閒去幹多件事。就比如上面的例子,A在等待的時候,CPU是閒着的,這時候這2s的時間內就可以去幹別的事情。其實多線程只是看起來是同時執行了多個任務,實際上只是利用的空閒時間去相互插空而已,只是我們感覺不到。

2、Java中怎麼實現多線程

其實這個問題在 面試 的時候也很容易被問到。有兩種方法,一種是繼承Thread類,一種是實現Runnable接口。其實Thread類也是實現了Runnable接口的,看一眼源碼就會知道了。

這裏實現起來其實很簡單,繼承Thread類就去重寫run方法,實現Runnable就去實現run方法,然後調用start方法執行線程。

這裏要說一點的是,多線程具有異步性,就是在代碼中寫的順序,不一定代表執行結果的順序。舉個例子,多個線程打印數字。

public class PrintText extends Thread {
    private int num = 0;
    public PrintText(int num) {
        this.num = num;
    }

    @Override
    public void run() {
        System.out.println(num);
    }
}

下面是入口程序。

public class Test {
    public static void main(String[] args) {
        PrintText1 pt1 = new PrintText(1);
        PrintText1 pt2 = new PrintText(2);
        PrintText1 pt3 = new PrintText(3);
        PrintText1 pt4 = new PrintText(4);
        pt1.start();
        pt2.start();
        pt3.start();
        pt4.start();
    }
}

這裏程序的執行順序是1、2、3、4,但是顯示的結果確實不確定的,因爲他們誰先佔用到了CPU的資源是不確定的,所以千萬不用想當然的把代碼的順序作爲了程序打印結果的依據。

3、併發訪問數據

講到多線程,肯定就要說說併發訪問數據的問題了。多個線程同時去修改一個數據的時候,很容易產生問題。其實這個東西我們接觸過,就是在學習 數據庫 的時候,有一個讀髒數據、不可重複讀那塊。

我們想想一個場景,兩個線程都要多一個數字做加一操作,比如這時候數字是5,那麼兩個線程同時讀到目前是5(其實肯定是有先後順序的,只不過這個時間差很短,短到兩個線程都還沒有完成加一併寫入的操作執行完),那麼每個線程各自加一得到6並寫入變量,那麼執行完的結果就是6,但是實際上兩個線程都進行了加一操作,正確的結果應該是7纔對。

如何解決這個問題呢?兩種辦法synchronized關鍵字和Lock,這個我們以後在說。

其實synchronized這個關鍵字就在我們身邊,只不過我們從沒注意過,我們總是在用,甚至一開始就學的System.out.println()這個打印語句,其中的println就是使用了這個關鍵字的一個線程 安全 的方法。

4、幾個基礎方法

下面我們來學幾個多線程的基礎方法,也就是Thread類給我們提供的一些方法。

4.1、currentThread()方法

這個方法會返回當前正在執行的線程的一些信息。其實也都很容易理解。

Thread.currentThread().getName(); // 返回當前線程的名稱
Thread.currentThread().getId(); // 返回當前線程的唯一標識

這裏需要注意的是,currentThread返回的是當前正在執行的線程的信息,而main方法的入口本身也是一個主線程。所以假設我們在上面打印數字的例子中,實現Thread類的構造方法中獲取當前的線程名字,會是main,而在run方法中獲取線程名字纔是當前這個類的線程,一般默認是Thread-0(這裏如果不給線程設置名稱的話,他默認按照線程創建順序把線程命名爲Thread-n這樣的形式)。

4.2、isAlive()方法

其實看英文也能差不多猜到,就是獲取當前線程是否在活動狀態。

如果直接在run()方法中判斷,一定會是true。說白了,就是run方法只要還在執行過程中,他就會是true。

4.3、sleep()方法

這個大家應該很熟悉吧,就是讓線程等待多久的方法,默認單位是毫秒。一般我們都是用的Thread.sleep(1000)這樣,其實他指的是currentThread的線程。

4.4、停止線程

在Java中有3中方法停止正在運行的線程。

a、用退出標誌位,使線程正常退出,也就是當run方法執行完後線程停止。 
b、stop方法強制停止。(這個方法已經棄用了,雖然很簡單,但是簡單必有坑) 
c、使用interrupt方法中斷線程。

你當然可以用退出標誌位來處理了,一般也就是各種if嵌套什麼的,邏輯會比較混亂;stop就不用說了,官方都棄用了;最後也就是推薦的使用interrupt中斷了。

使用interrupt中斷,可沒有for循環中的break那麼簡單,調用thread的interrupt方法,並不會立刻終端線程,而只是標記這個線程要中斷,能不能中斷還是要看線程當前的狀態的。你覺得這樣不好?stop就是因爲可以立刻中斷,讓線程沒有好好辦法善後,所以才推薦用interrupt的呀~

配合interrupt()方法的還有幾個方法,他們是interrupted,isInterrupted,看起來好像是一個意思是吧。其實不一樣,前者意思是當前線程是否已經中斷,後者是線程是否已經中斷。看一下程序的聲明,會發現,前者是 靜態(static) 方法,那肯定就是Thread可以直接調用了,所以默認也就是當前線程了;後者是類的普通方法,那麼肯定要在某個類的實例化之後才能調用,所以就是那個線程的判斷了。

這裏還要有一點需要注意,就是這個interrupted方法,如果在線程已經是中斷的狀態下的話連續調用兩次,第一次會是true,第二次就回事false了。爲什麼呢?官方給出的解釋是這個方法具有清楚狀態的功能。所以第一次返回true,並且清除了狀態,所以第二次就返回false了,這點要特別注意。

多嘴一句,isInterrupted是不會清除狀態的。

所以我們我們怎麼通過interrupt去結束線程呢?當然可以在run方法裏面通過interrupted方法的判斷來做if,但是這樣真的會有些混亂,所以推薦的是 異常法 

先舉例一個if判斷的壞處。

public class PrintText extends Thread {

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            if (this.interrupted()) {
                System.out.println("interrupted");
                break;
            }
            System.out.println(i);
        }
        System.out.println("finish");
    }
}

上面的代碼,如果程序出現中斷了,那麼會跳出for循環,但是如果想不打印最後的finish字符內容的話,怎麼處理呢?在做一個判斷嗎?

如果這裏我們 把break改爲拋出一個異常 ,然後在整個代碼塊上做個try...catch,這樣,就可以不打印finish信息了。如果想打印的話,可以讓try...catch不包含這個打印語句,這樣就可以打印出來了,所以異常法更靈活一些吧。當然你這裏也可以使用return不打印finish,但是也不夠靈活。

4.5、睡眠中停止

如果線程正處於sleep狀態的時候,調用線程的interrupt,sleep會自動拋出InterruptedException,這樣同樣可以達到我們的目的。但是這種情況需要注意的是,拋出異常的同時也會把中斷狀態清除,這點要格外留意。

另外這裏我給大家總結一點,就是sleep和interrupt兩個方法的執行前後所產生的不同結果。

上面說的是在sleep過程中調用interrupt,會拋出異常中斷;還有一種情況,就是先interrupt,然後線程仍會執行,這時候調用sleep,立即也會產生中斷異常。

4.6、暫停與恢復

暫停與恢復應該很好理解,就是讓線程的運行暫停或者恢復。對應的方法分別爲suspend與resume,但是需要說的也是一樣,這兩個方法也已經被官方棄用了,棄用就說明一定有他不好的地方。使用起來很簡單,這裏就不做介紹了,大家可以自己試試。

這裏說一下他們的缺點,比如有一個線程鎖定了一個對象,然後暫停了,會造成對這個對象的長時間鎖定,可能會堵死程序。另外也會造成數據的不一致,比如一個線程需要對這個數據做兩次加一,結果只做了一次的時候暫停了,別的程序拿到的不是一個正確的結果,就出錯了。

4.7、yield方法

這個方法意思就是主動放棄當前CPU資源,讓其他線程使用。但是這裏的其他線程也包括他自己的,所以並不是一個精準的功能。但是會造成CPU資源切換所導致的時間花銷。

4.8、線程的優先級

線程可以設置優先級,有個方法是setPriority(int newPriority)。

線程優先級只有10級就是1到10,如果超出範圍會拋出異常的,這點大家可以看一下源碼瞭解。默認的優先級都是5,但是這裏需要說明一點的是,優先級並不會絕對代表程序的執行順序,只是個建議,實際上可能會和我們設置的不一樣。但是跨度很大的比如1和10的話,10比1先執行的可能性就大很多。

另外線程的 優先級具有繼承性 ,比如A線程優先級是10,那麼由A線程啓動的B線程,優先級也會是10。

4.9、守護線程

守護線程就是一種特殊的線程,他依託於某一線程A存在,當A線程結束的時候,守護線程也會自動結束。

具體用法就也很簡單,就是通過調用thread.setDaemon(true)來設置的。

到此,所有有關多線程的基礎內容也就介紹完了,希望大家能夠掌握。

轉載來源: 趙伊凡's Blog 

發佈了10 篇原創文章 · 獲贊 6 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章