前言:雖然工作了三年,但是幾乎沒有使用到多線程之類的內(nèi)容。這其實(shí)是工作與學(xué)習(xí)的矛盾。我們在公司上班,很多時(shí)候都只是在處理業(yè)務(wù)代碼,很少接觸底層技術(shù)。

 

可是你不可能一輩子都寫業(yè)務(wù)代碼,而且跳槽之后新單位很可能有更高的技術(shù)要求。除了干巴巴地翻書,我們可以通過兩個(gè)方式來解決這個(gè)問題:一是做業(yè)余項(xiàng)目,例如在github上傳自己的demo,可以實(shí)際使用;二是把自己的學(xué)習(xí)心得寫成博客,跟同行們互相交流。

 

3.1 線程的初窺門徑

我們在之前的文章里提到的程序其實(shí)都是單線程程序,也就說啟動(dòng)的程序從main()程序進(jìn)入點(diǎn)開始到結(jié)束只有一個(gè)流程。然而,有時(shí)候我們設(shè)計(jì)的程序需要有多個(gè)流程,也就是所謂的多線程(Multi-thread)程序。

 

我們可以通過一個(gè)龜兔賽跑的游戲開始學(xué)習(xí)單線程和多線程的區(qū)別。烏龜和兔子賽跑,起點(diǎn)到終點(diǎn)為10米,每經(jīng)過一秒,烏龜會(huì)前進(jìn)0.1米;兔子可能前進(jìn)1米或睡覺。如果使用單線程,我們可以這樣設(shè)計(jì):

移動(dòng)開發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

 1 /**
 2  * 線程實(shí)驗(yàn)用例
 3  */
 4 public class TortoiseHareRace {
 5     public static void main(String[] args) throws InterruptedException {
 6         float totalLength = 10;
 7         float tortoiseLength = 0;
 8         float hareLength = 0;
 9         System.out.println("龜兔賽跑大賽,開始!");
10         while(tortoiseLength < totalLength && hareLength < totalLength) {
11             Thread.sleep(1000);
12             tortoiseLength += 0.1;
13             System.out.println("烏龜跑了 " + tortoiseLength + " 米...");
14             boolean isHareSleep = Math.random()*10 < 9;
15             if(isHareSleep) {
16                 System.out.println("兔子睡著了zzzz");
17             } else {
18                 hareLength ++;
19                 System.out.println("兔子跑了 " + hareLength + " 米...");
20             }
21         }
22     }
23 }

移動(dòng)開發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

 

這個(gè)程序里面只有一個(gè)流程,就是從main()方法開始到結(jié)束的流程。要讓目前流程暫停指定時(shí)間,可以使用java.lang.Thread的靜態(tài)sleep()方法,單位是毫秒。調(diào)用這個(gè)方法必須處理java.lang.InterruptedException,在這里直接在main()中聲明 throws,由JVM來處理此異常。

 

每次暫停一秒后,tortoiseLength遞增0.1,表示烏龜向前爬了0.1米,兔子則可能有百分之90的可能性睡覺。如果不睡覺,hareLength會(huì)遞增1,表示兔子向前蹦了1米。只要他們?nèi)我庖粋€(gè)跑了10米,表示到達(dá)終點(diǎn),分出勝負(fù)比賽結(jié)束。

 

由于程序只有一個(gè)流程,所以每次只能先讓烏龜先跑再讓兔子跑,這就很不公平(如果倒過來,也是不公平)。如果程序里再有兩個(gè)流程,一個(gè)是烏龜在跑,一個(gè)是兔子在跑,是不是程序邏輯會(huì)更加合理和清晰呢?

 

在Java中,如果想在main()以外獨(dú)立設(shè)計(jì)流程,可以實(shí)現(xiàn)java.lang.Runnable接口,流程的進(jìn)入點(diǎn)是在run()方法里面。例如,我們可以這樣設(shè)計(jì)烏龜?shù)牧鞒蹋?nbsp;

移動(dòng)開發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

 1 /**
 2  * 烏龜線程
 3  */
 4 public class Tortoise implements Runnable{
 5     private float totalLength;
 6     private float length;
 7 
 8     public Tortoise(float totalLength) {
 9         this.totalLength = totalLength;
10     }
11 
12     @Override
13     public void run() {
14         try {
15             while(length < totalLength) {
16                 Thread.sleep(1000);
17                 length += 0.1;
18                 System.out.println("烏龜跑了 " + length + " 米...");
19             }
20         } catch (InterruptedException ex) {
21             throw new RuntimeException(ex);
22         }
23     }
24 }

移動(dòng)開發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

 

在Tortoise類中,烏龜?shù)牧鞒虝?huì)從run()開始。在這個(gè)流程里,代碼只需要專心負(fù)責(zé)烏龜每秒爬0.1米就可以了,不用夾雜兔子的動(dòng)作。同樣地,我們可以類似地設(shè)計(jì)兔子的流程:

移動(dòng)開發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

 1 /**
 2  * 兔子線程
 3  */
 4 public class Hare implements Runnable{
 5     private float totalLength;
 6     private float length;
 7 
 8     public Hare(int totalLength) {
 9         this.totalLength = totalLength;
10     }
11 
12     @Override
13     public void run () {
14         try {
15             while(length < totalLength) {
16                 Thread.sleep(1000);
17                 boolean isHareSleep = Math.random()*10 < 9;
18                 if(isHareSleep) {
19                     System.out.println("兔子睡著了zzzz");
20                 } else {
21                     length ++;
22                     System.out.println("兔子跑了 " + length + " 米...");
23                 }
24             }
25         } catch (InterruptedException ex) {
26             throw new RuntimeException(ex);
27         }
28     }
29 }

移動(dòng)開發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

 

在Java中,從main()方法開始的流程會(huì)由主線程(Main Thread)來跑。那么剛才設(shè)計(jì)的兔子線程和烏龜線程,應(yīng)該讓誰來執(zhí)行呢?我們可以通過創(chuàng)建Thread對象的方法來執(zhí)行Runnable對象定義的run()方法,例如:

移動(dòng)開發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

 1 /**
 2  * 龜兔賽跑主線程
 3  */
 4 public class TortoiseHare2 {
 5     public static void main(String[] args) {
 6         Tortoise tortoise = new Tortoise(10);
 7         Hare hare = new Hare(10);
 8         Thread tortoiseThread = new Thread(tortoise);
 9         Thread hareThread = new Thread(hare);
10         tortoiseThread.start();
11         hareThread.start();
12     }
13 }

移動(dòng)開發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

 

要記住,在創(chuàng)建Thread對象之前,必須確保實(shí)例化的參數(shù)實(shí)現(xiàn)了Runnable接口,并且要在之后執(zhí)行start()方法,以下是其中一個(gè)執(zhí)行結(jié)果:

移動(dòng)開發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

 

我們都在學(xué)校里學(xué)習(xí)過線程的概念,但這都只是邏輯上的認(rèn)識(shí),并不代表我們已經(jīng)完全掌握其實(shí)際內(nèi)容。通過做實(shí)驗(yàn)的方式,我們寫了代碼,引入了耳熟能詳?shù)膬和适?,加深認(rèn)識(shí)的同時(shí)也增強(qiáng)了記憶。

 

3.2繼承父類 or 實(shí)現(xiàn)接口

從抽象觀點(diǎn)與開發(fā)者的角度來看,JVM是臺(tái)虛擬計(jì)算機(jī),只安裝了一顆被稱為主線程的CPU,可執(zhí)行main()定義的執(zhí)行流程。如果想要為JVM加裝CPU,就是創(chuàng)建Thread實(shí)例,要啟動(dòng)額外CPU就是調(diào)用Thread實(shí)例的start()方法。額外CPU執(zhí)行流程的進(jìn)入點(diǎn),可以定義在Runnable接口的run()方法里面。

 

當(dāng)然了,實(shí)際上JVM啟動(dòng)之后并不只有一個(gè)主線程,還有垃圾收集、內(nèi)存管理等線程,不過這是底層機(jī)制。我們暫時(shí)不用理會(huì)。

 

除了將流程定義在Runnable的run()方法之外,還有另外一個(gè)使用多線程的方法,就是繼承Thread類,重新定義run()方法。例如我們可以這樣的改寫烏龜流程:

移動(dòng)開發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

 1 /**
 2  * 烏龜?shù)木€程
 3  */
 4 public class Tortoise extends Thread{ //繼承Thread類
 5     private float totalLength;
 6     private float length;
 7 
 8     public Tortoise(float totalLength) {
 9         this.totalLength = totalLength;
10     }
11 
12     @Override
13     public void run() {
14         try {
15             while(length < totalLength) {
16                 Thread.sleep(1000);
17                 length += 0.1;
18                 System.out.println("烏龜跑了 " + length + " 米...");
19             }
20         } catch (InterruptedException ex) {
21             throw new RuntimeException(ex);
22         }
23     }
24 }

移動(dòng)開發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

 

由于大部分代碼都一樣,所以就不再把整個(gè)demo重新貼出來一遍了。但還是建議各位朋友,尤其是初學(xué)者們,千萬要自己動(dòng)手改一改,看看兩種寫法到底有什么不同。

 

在Java中,任何線程可執(zhí)行的流程都要定義在Runnable的run()方法。事實(shí)上,Thread類本身也實(shí)現(xiàn)了Runnable接口,從JDK源碼當(dāng)中我們可以看到:

移動(dòng)開發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

 1     /**
 2      * If this thread was constructed using a separate
 3      * <code>Runnable</code> run object, then that
 4      * <code>Runnable</code> object's <code>run</code> method is called;
 5      * otherwise, this method does nothing and returns.
 6      * <p>
 7      * Subclasses of <code>Thread</code> should override this method.
 8      *
 9      * @see     #start()
10      * @see     #stop()
11      * @see     #Thread(ThreadGroup, Runnable, String)
12      */
13     @Override
14     public void run() {
15         if (target != null) {
16             target.run();
17         }
18     }

移動(dòng)開發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

 

了解完兩種多線程的實(shí)現(xiàn)方法之后,我們會(huì)有一個(gè)疑問:到底是實(shí)現(xiàn)Runnable()好呢,還是直接繼承Thread類更好?這就要根據(jù)具體情況具體分析了。

 

實(shí)現(xiàn)接口的好處是靈活,這個(gè)類還可以繼承其他類。如果你想要直接利用Thread中定義的某些特定方法,那就可以考慮直接繼承Thread類。要想做到靈活應(yīng)用,這就要求開發(fā)者足夠了解這些接口和類。沒有別的捷徑,就是多看API說明文檔和多閱讀JDK源代碼。

 

3.3 線程的生命周期

有一個(gè)古老的謎語,說是有一種動(dòng)物,早上是四條腿,中午就成了兩條腿,到了傍晚卻是三條腿。我們大家都知道,謎底就是人。

 

從小孩到長大成人,再到衰老死亡,這就是我們?nèi)祟惖纳芷?。線程也是一樣,從創(chuàng)建到開始,再到最后的結(jié)束。理解人類的生命周期,有助于我們更好地認(rèn)識(shí)自己,規(guī)劃自己的人生。熟悉線程的生命周期,則會(huì)對我們使用線程有莫大的幫助。

 

線程的生命周期相當(dāng)復(fù)雜,我們將會(huì)從最簡單的開始講起。

 

3.3.1 Daemon線程

Daemon,本義是守護(hù)神的意思,在希臘神話里是半人半神的精靈。在計(jì)算機(jī)領(lǐng)域,daemon已經(jīng)是一個(gè)專業(yè)術(shù)語,我們可以理解成守護(hù)進(jìn)程或守護(hù)線程。

 

大人物出現(xiàn)活動(dòng),如果他不離場,安保人員是不可能會(huì)撤離的。Java中的守護(hù)進(jìn)程也一樣,如果主線程中啟動(dòng)了額外線程,默認(rèn)會(huì)等待被啟動(dòng)的所有線程都執(zhí)行完run()方法之后才中止JVM。如果一個(gè)Thread被標(biāo)識(shí)為Daemon線程,那么在所有的非Daemon線程都結(jié)束時(shí),它也會(huì)被JVM自動(dòng)終止。

 

從main()方法開始的就是一個(gè)非Daemon線程。我們可以使用setDaemon()方法來設(shè)定一個(gè)線程是否屬于Daemon線程。下面是一個(gè)非常簡單的demo,實(shí)驗(yàn)的時(shí)候你可以試著取消setDaemon()那行代碼的注釋,看看會(huì)有什么不同,為什么:

移動(dòng)開發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

 1 /**
 2  * Daemon實(shí)驗(yàn)用例
 3  */
 4 public class DaemonDemo {
 5     public static void main(String[] args) {
 6         Thread thread = new Thread() {
 7             public void run() {
 8                 while(true) {
 9                     System.out.println("run...");
10                 }
11             }
12         };
13         //thread.setDaemon(true); //把線程設(shè)定為Daemon線程
14         thread.start();
15     }
16 }

移動(dòng)開發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

 

如果沒有使用setDaemon()方法設(shè)定為true,上面這個(gè)程序就會(huì)不會(huì)輸出“run...”,永遠(yuǎn)不會(huì)自動(dòng)終止??墒侨绻坏⑵湓O(shè)置為Daemon線程,在其啟動(dòng)的一瞬間就會(huì)被終止,甚至連一句輸出都沒有。

 

有趣的是,默認(rèn)所有從Daemon線程產(chǎn)生的線程也是Daemon線程。其實(shí)這也不難理解,因?yàn)榛旧蟻碚f由一個(gè)后臺(tái)服務(wù)線程衍生出來的線程,也應(yīng)該是為了在后臺(tái)服務(wù)而產(chǎn)生的,所以在產(chǎn)生它的線程停止時(shí),也應(yīng)該一并跟著停止。

 

3.3.2 線程狀態(tài)圖

在看過最簡單的線程生命周期之后,我們再來看復(fù)雜一點(diǎn)的。調(diào)用Thread對象的start()方法之后,該線程的基本狀態(tài)基本可以分為三種:可執(zhí)行(Runnable)、別阻斷(Blocked)、執(zhí)行中(Running)。

 

三種狀態(tài)之間的轉(zhuǎn)移一兩句話說不清楚,我們可以借助狀態(tài)圖來幫助我們理解:

移動(dòng)開發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)     

 

我們可以一邊對照著上面這幅圖,一邊看接下來的講述。實(shí)例化Thread并執(zhí)行start()方法之后,線程就會(huì)進(jìn)入Runnable狀態(tài)。這個(gè)時(shí)候線程并沒有真正開始執(zhí)行run()方法,必須等待排班器(Scheduler)排入CPU執(zhí)行,線程才會(huì)跑run()方法,這就進(jìn)入了Running狀態(tài)。

 

線程有優(yōu)先權(quán)重,可以使用Thread的setPriority()方法來設(shè)定優(yōu)先權(quán)。設(shè)定的范圍是1(Thread.MIN_PRIORITY)到 10(Thread.MAX_+PRIORITY),默認(rèn)是5(Thread.NORM_PRIORITY)。數(shù)字越大優(yōu)先權(quán)越高,調(diào)度器越有限排入CPU,也就是越優(yōu)先進(jìn)入Running狀態(tài)。如果優(yōu)先權(quán)相同,則輪流執(zhí)行(Round-robin)。

 

有幾種情況會(huì)讓線程進(jìn)入Blocked狀態(tài),例如前面調(diào)用過的Thread.sleep()方法,又例如等待輸入輸出等等。我們之所以要運(yùn)用多線程,就是要讓當(dāng)前線程進(jìn)入Blocked狀態(tài)的時(shí)候讓另一線程排入CPU執(zhí)行,即進(jìn)入Running狀態(tài),避免CPU無謂的空閑。這就是我們常用的,用來改進(jìn)性能的方式之一。

 

下面我們可以用一個(gè)下載網(wǎng)頁的demo來進(jìn)行測試,看看不使用多線程時(shí)需要花費(fèi)多長時(shí)間:

移動(dòng)開發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

 1 import java.net.URL;
 2 import java.io.*;
 3 import java.util.Date;
 4 
 5 /**
 6  * 單線程實(shí)驗(yàn)用例
 7  */
 8 public class Download {
 9     public static void main(String[] args) throws Exception {
10         URL[] urls = {
11                 new URL("http://www.cnblogs.com/levenyes/p/7117559.html"),
12                 new URL("http://www.cnblogs.com/levenyes/p/7120267.html"),
13                 new URL("http://www.cnblogs.com/levenyes/p/7145214.html"),
14                 new URL("http://www.cnblogs.com/levenyes/p/7163843.html")
15         };
16 
17         String[] fileNames = {
18                 "file1.html",
19                 "file2.html",
20                 "file3.html",
21                 "file4.html"
22         };
23         Date begin = new Date();
24         for(int i = 0 ; i < urls.length; i++) {
25             dump(urls[i].openStream(), new FileOutputStream(fileNames[i]));
26         }
27         Date end = new Date();
28         System.out.println(end.getTime() - begin.getTime());
29     }
30 
31     private static void dump(InputStream src, OutputStream dest) throws IOException {
32         try (InputStream input = src; OutputStream output = dest) {
33             byte[] data = new byte[1024];
34             int length = -1;
35             while((length = input.read(data)) != -1) {
36                 output.write(data, 0, length);
37             }
38         }
39     }
40 }

移動(dòng)開發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

 

每一次for循環(huán)時(shí),會(huì)先開啟網(wǎng)絡(luò)鏈接、進(jìn)行HTTP請求,然后再進(jìn)行文檔寫入等等。在等待網(wǎng)絡(luò)鏈接、HTTP協(xié)議時(shí)很耗時(shí),這就意味著進(jìn)入Blocked的時(shí)間相當(dāng)長。因?yàn)槭菃尉€程,所以必須等第一個(gè)網(wǎng)頁下載完了之后才能下載第二個(gè),如此類推。

 

因?yàn)槭艿骄W(wǎng)絡(luò)狀態(tài)不穩(wěn)定的影響,有的時(shí)候可能會(huì)比較快,有的時(shí)候會(huì)比較慢。經(jīng)過多次測試,大概的耗時(shí)為400毫秒。

 

如果我們可以在下載第一個(gè)網(wǎng)頁遇到等待時(shí)就開始下載其他網(wǎng)頁,這樣會(huì)不會(huì)讓速度加快許多呢?例如下面這個(gè)demo:

移動(dòng)開發(fā)培訓(xùn),Android培訓(xùn),安卓培訓(xùn),手機(jī)開發(fā)培訓(xùn),手機(jī)維修培訓(xùn),手機(jī)軟件培訓(xùn)

 1 import java.net.URL;
 2 import java.io.*;
 3 import java.util.Date;
 4 
 5 /**
 6  * 多線程實(shí)驗(yàn)用例
 7  */
 8 public class Download {
 9     public static void main(String[] args) throws Exception {
10         final URL[] urls = {
11                 new URL("http://www.cnblogs.com/levenyes/p/7117559.html"),
12                 new URL("http://www.cnblogs.com/levenyes/p/7120267.html"),
13                 new URL("http://www.cnblogs.com/levenyes/p/7145214.html"),
14                 new URL("http://www.cnblogs.com/levenyes/p/7163843.html")
15         };
16 
17         final String[] fileNames = {
http://www.cnblogs.com/levenyes/p/7211461.html