Android中的線程形態(tài)(一)(進程/線程/線程池)

本篇提綱.png

一.線程與進程相關(guān)

1.進程

??定義:進程是具有獨立功能的程序關(guān)于某個數(shù)據(jù)集合上的一次運行活動蒸绩,進程是操作系統(tǒng)分配資源的單位。
??當(dāng)你運行一個程序,你就啟動了一個進程及穗。顯然肝箱,程序只是一組指令的有序集合哄褒,它本身沒有任何運行的含義,只是一個靜態(tài)實體煌张。而進程則不同呐赡,它是程序在某個數(shù)據(jù)集上的執(zhí)行,是一個動態(tài)實體骏融。它因創(chuàng)建而產(chǎn)生链嘀,因調(diào)度而運行萌狂,因等待資源或事件而被處于等待狀態(tài)伦腐,因完成任務(wù)而被撤消腐魂,反映了一個程序在一定的數(shù)據(jù)集上運行的全部動態(tài)過程。

特點:

  • 進程是程序的一次執(zhí)行過程禾锤!過程包个!~~活動的刷允。
  • 系統(tǒng)資源(如內(nèi)存、文件)以進程為單位分配碧囊。
  • 操作系統(tǒng)為每個進程分配了獨立的地址空間树灶。

2.線程

??定義:線程是操作系統(tǒng)能夠進行運算調(diào)度的最小單位。它被包含在進程之中糯而,是進程中的實際運作單位天通。線程是操作系統(tǒng)調(diào)度和分派的基本單位。
??線程是屬于進程的熄驼,線程自己是沒有內(nèi)存空間的像寒,它運行在進程空間內(nèi)。同一進程所產(chǎn)生的線程共享同一內(nèi)存空間瓜贾,當(dāng)進程退出時該進程所產(chǎn)生的線程都會被強制退出并清除诺祸。
??線程可與屬于同一進程的其它線程共享進程所擁有的全部資源,但是其本身基本上不擁有系統(tǒng)資源祭芦,只擁有一點在運行中必不可少的信息(如程序計數(shù)器筷笨、一組寄存器和棧)。

為什么要有線程龟劲?
??首先我們要明確一點胃夏,CPU計算的速度是非常非常快的昌跌,寄存器僅僅能夠追的上他的腳步仰禀,RAM和別的掛在各總線上的設(shè)備更是難以望其項背。因此當(dāng)多個任務(wù)需要執(zhí)行的時候蚕愤,就需要輪流著來悼瘾,同時運行多個進程,即并發(fā)技術(shù)审胸。實現(xiàn)并發(fā)技術(shù)相當(dāng)復(fù)雜亥宿,最容易理解的是“時間片輪轉(zhuǎn)進程調(diào)度“
??在操作系統(tǒng)的管理下,所有正在運行的進程輪流使用CPU砂沛,每個進程允許占用CPU的時間非常短(比如10毫秒)烫扼,這樣用戶根本感覺不出來CPU是在輪流為多個進程服務(wù),就好象所有的進程都在不間斷地運行一樣碍庵。但實際上在任何一個時間內(nèi)有且僅有一個進程占有CPU映企。
??進程是操作系統(tǒng)分配資源(內(nèi)存空間悟狱,文件)的基本單位,進程所分配的空間在不同的進程之間是相互獨立的堰氓。嘉禾上邊說的“時間片輪轉(zhuǎn)進程調(diào)度”挤渐,可以知道,系統(tǒng)在不同進程之間切換的時候双絮,必然要經(jīng)過“開始執(zhí)行A進程浴麻,保存進程A的上下文,調(diào)入下一個要執(zhí)行的進程B的上下文囤攀,然后開始執(zhí)行進程B,保存進程B的上下文”软免,上下文就是進程所處的環(huán)境,系統(tǒng)切出去執(zhí)行另一個進程之后一段時間要切回來焚挠,而切回來的依據(jù)就是原來進程的所處的環(huán)境(得知道原來那個進程地方在哪膏萧,執(zhí)行到哪了等)。由于A蝌衔,B兩個進程所屬的系統(tǒng)空間榛泛、占用的資源都是相互獨立的,因此這個切換不同進程上下文的過程所消耗的資源就比較大噩斟。而線程是在統(tǒng)一進程內(nèi)部的挟鸠,同一進程不同線程之間共用一段內(nèi)存,貢獻同一資源亩冬,所以線程之間的額切換顯然要比進程之間的切換容易的多。
??同時硼身,一個進程不只是做一個任務(wù)硅急,我呢可能會有不同的任務(wù)需求。比如我打開一個QQ佳遂,可能我一遍下載文件营袜,一遍發(fā)送語音,一遍打字——這里QQ就可以看做是一個進程丑罪,而下文件荚板,發(fā)語音,發(fā)文字是由三個不同的線程完成的吩屹。

總結(jié)一下跪另,引入線程有下面三方面的考慮:

  • 應(yīng)用的需要。比如打開一個QQ煤搜,可能我一遍下載文件免绿,一遍發(fā)送語音,一遍打字擦盾。如果QQ是一個進程嘲驾,那么這樣的需求需要線程機制淌哟。
  • 開銷的考慮。在進程內(nèi)創(chuàng)建辽故、終止線程比創(chuàng)建徒仓、終止進程要快。同一進程內(nèi)的線程間切換比進程間的切換要快,尤其是用戶級線程間的切換誊垢。線程之間相互通信無須通過內(nèi)核(同一進程內(nèi)的線程共享內(nèi)存和文件)
  • 性能的考慮掉弛。多個線程中,任務(wù)功能不同(有的負責(zé)計算彤枢,有的負責(zé)I/O),如果有多個處理器狰晚,一個進程就可以有很多的任務(wù)同時在執(zhí)行。

線程的特點:

  • 有自己的棧和棧指針
  • 共享所在進程的地址空間和其它資源
  • 不運行時需要保存線程上下文環(huán)境(需要程序計數(shù)器等寄存器,和進程一樣缴啡,切回來的時候得知道之前的線程執(zhí)行到哪了)
  • 有標(biāo)識符ID(如JAVA中Thread.currentThread())

3.一些通俗的解釋

??上面說了一大堆壁晒,實際上,線程和進程本質(zhì)上是CPU兩種不同的工作時間段的描述业栅,只不過顆粒大小不同秒咐。為什么這么說呢?上面我們反復(fù)強調(diào)過很多遍:進程是操作系統(tǒng)分配資源的基本單位碘裕,線程是操作系統(tǒng)運算調(diào)度的基本單位携取。
??說的跟通俗一點,CPU正真“時間片輪轉(zhuǎn)調(diào)度”的是線程帮孔,真正處理工作的地方也是線程雷滋。但是線程是屬于進程的,加入有兩個進程A和B文兢,每個進程中都有兩個線程A1晤斩,A2,B1姆坚,B2澳泵。CPU的執(zhí)行時間就在A1,A2兼呵,B1兔辅,B2這四個線程之間輪轉(zhuǎn),如果不慎從A1切換到B2击喂,那么也就是進程A切換到了進程B维苔。

從三個角度來剖析二者之間的區(qū)別:

  • 調(diào)度:線程作為調(diào)度和分配的基本單位,進程作為擁有資源的基本單位懂昂。
  • 并發(fā)性:不僅進程之間可以并發(fā)執(zhí)行蕉鸳,同一個進程的多個線程之間也可以并發(fā)執(zhí)行。
  • 擁有資源:進程是擁有資源的一個獨立單位,線程不擁有系統(tǒng)資源潮尝,但可以訪問隸屬于進程的資源榕吼。

4.Android中的線程與進程

??在Android系統(tǒng)中,每一個App都是一個Linux用戶勉失。一般情況下羹蚣,每個App都是運行在一個進程的一個線程中,這個線程習(xí)慣稱為主線程或者UI線程(注意乱凿,一個進程的一個線程之中)顽素。
??Zygote是一個虛擬機進程,同時也是一個虛擬機實例的孵化器徒蟆,每當(dāng)系統(tǒng)要求執(zhí)行一個 Android應(yīng)用程序胁出,Zygote就會FORK出一個子進程來執(zhí)行該應(yīng)用程序。
??Zygote進程是在系統(tǒng)啟動時產(chǎn)生的段审,它會完成虛擬機的初始化全蝶,庫的加載,預(yù)置類庫的加載和初始化等等操作寺枉,而在系統(tǒng)需要一個新的虛擬機實例時抑淫,Zygote通過復(fù)制自身,最快速的提供個系統(tǒng)姥闪。

以上內(nèi)容參考:
騰訊面試題04.進程和線程的區(qū)別始苇?
android 線程與進程 區(qū)別 聯(lián)系
線程和進程的區(qū)別是什么? 知乎.zhonyong的回答

二.線程與線程池

1.線程

??Java中有兩種創(chuàng)建線程的方式筐喳,即我們所熟知的繼承thread類實現(xiàn)Runnable接口催式。于是乎就來了一個非常“經(jīng)典”并且并用爛了的例子——買票避归!這里我們也展示一下:

①繼承Thread類:
public class TicketThread extends Thread{
    private int ticket = 10;
    private String name;

    public TicketThread(String name){
        this.name =name;
    }

    public void run(){
         for(int i =0;i<500;i++){
                if(this.ticket>0){
                    System.out.println(this.name+"賣票---->"+(this.ticket--));
                }
          }
    }

    public static class ThreadDemo {
           public static void main(String[] args) {
               TicketThread mt1= new TicketThread("一號窗口");
               TicketThread mt2= new TicketThread("二號窗口");
               TicketThread mt3= new TicketThread("三號窗口");
               mt1.start();
               mt2.start();
               mt3.start();
            }
        }
}

結(jié)果是:

一號窗口賣票---->10
一號窗口賣票---->9
一號窗口賣票---->8
三號窗口賣票---->10
二號窗口賣票---->10
二號窗口賣票---->9
二號窗口賣票---->8
二號窗口賣票---->7
二號窗口賣票---->6
二號窗口賣票---->5
......

后面的一串結(jié)果我就不貼了荣月,意思就是說,票被賣重復(fù)了槐脏,每張票都賣了三遍。

②實現(xiàn)Runnable接口:
public class TicketRunnable implements Runnable{
    private int ticket =10;  
    private String name;  

    @Override
    public void run() {
        // TODO Auto-generated method stub
        for(int i =0;i<500;i++){  
            if(this.ticket>0){  
                System.out.println(Thread.currentThread().getName()+"賣票---->"+(this.ticket--));  
            }  
        }  
    }

    public static  class RunnableDemo {     
        public static void main(String[] args) {  
            // TODO Auto-generated method stub  
            //設(shè)計三個線程  
            TicketRunnable mt = new TicketRunnable(); 
             Thread t1 = new Thread(mt,"一號窗口");  
             Thread t2 = new Thread(mt,"二號窗口");  
             Thread t3 = new Thread(mt,"三號窗口");  
             t1.start();  
             t2.start();  
             t3.start();  
        }
    }
}

結(jié)果為:

二號窗口賣票---->10
二號窗口賣票---->8
三號窗口賣票---->9
二號窗口賣票---->7
三號窗口賣票---->6
二號窗口賣票---->5
二號窗口賣票---->3
二號窗口賣票---->2
三號窗口賣票---->4
二號窗口賣票---->1

結(jié)果剛好撇寞,每張票賣一次顿天。

??于是就有博客說了,上面兩種實現(xiàn)方式蔑担,繼承Thread類是各自線程賣三份票牌废,會把票賣重復(fù)了;實現(xiàn)Runnable接口是三個線程賣同一份票啤握,所以結(jié)果正確——這說了好像跟沒說一樣鸟缕??!更有甚者說懂从,第一種方法中“保證安全的方法:把賣票的步驟用synchronized包起來授段。那么就不會出問題了”——你在逗我?番甩?侵贵!

??好吧~~我們來看看這兩種方法——事實上,不論是繼承Thread類還是實現(xiàn)Runnable接口缘薛,其本質(zhì)都要:①重寫Runnale接口中的Run方法窍育,在其中定義我們在線程中具體要做的事情。②調(diào)用Thread.start()方法從系統(tǒng)中new一個線程出來宴胧。
我們可以看下漱抓。
不信你回過頭去看看上面兩端代碼,都做了這兩件事情恕齐。
??我們可以看下Thread類源碼:

public class Thread implements Runnable {

看到了吧乞娄?Thread類也實現(xiàn)了Runnable接口,而Runnable接口:

public interface Runnable {
    public abstract void run();
}

就兩句代碼檐迟,也就是抽象的run()方法补胚,所以無論你是繼承的Thread類還是直接實現(xiàn)的Runnable方法,實際上最終都要重寫其中的run方法追迟。我們回到Thread類中溶其,看看我們的new Thread()也就是構(gòu)造函數(shù):

    public Thread(String name) {    //上述第一種方法
        init(null, null, name, 0);
    }

    public Thread(Runnable target, String name) {   //上述第二種方法
        init(null, target, name, 0);
    }

我們主要要看到這個target就是我們傳進去的Runnable對象。我們接下來直接看start()方法敦间。

    public synchronized void start() {

        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        group.add(this);

        started = false;
        try {
            nativeCreate(this, stackSize, daemon);
            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 */
            }
        }
    }

這個段代碼中nativeCreate(this, stackSize, daemon);這句代碼就是向系統(tǒng)請求創(chuàng)建一個線程的方法瓶逃,這是一個native方法,我們不作分析廓块。然后再看我們都要重寫的run方法:

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

看到了吧厢绝?我沒騙你吧~~重寫的run方法中調(diào)用了target.run();,也就是我們new Thread(mt,"一號窗口");穿進去的mt這個Runnable對象带猴。
??所以昔汉,顯而易見,上述兩個結(jié)論是正確的:①重寫Runnale接口中的Run方法拴清,在其中定義我們在線程中具體要做的事情靶病。②調(diào)用Thread.start()方法從系統(tǒng)中new一個線程出來。
我們可以看下口予。

??至于為什么第一種情況會出現(xiàn)票重復(fù)賣的情況而第二種沒有呢娄周?這主要是因為,第一種情況中:

public class TicketThread extends Thread{
    private int ticket = 10;

這個ticket是TicketThread類的實例變量沪停,而我們在start這個線程的時候煤辨,通過new TicketThread("一號窗口");裳涛,new TicketThread("二號窗口");TicketThread mt3= new TicketThread("三號窗口");看到?jīng)]有众辨,這里每new一個TicketThread對象端三,都會把該類中的實例變量拷貝一份到自己的內(nèi)存中,new了三次泻轰,也就拷貝了三份ticket到三個對象中技肩,然后start之后當(dāng)然是每個線程跑自己線程中的ticket,所以就出現(xiàn)跑重復(fù)了浮声;
??如果我們把ticket聲明為static類型虚婿,即private static int ticket = 10;,再跑一遍泳挥,結(jié)果就和第二種情況一樣了然痊!這是因為,靜態(tài)成員是屬于整個類的屉符,不是屬于對象的剧浸。類加載的時候,JAW就會給靜態(tài)成員分配一個特定的內(nèi)存空間矗钟,所有之后取用這個靜態(tài)成員的時候唆香,都會去這個特定的內(nèi)存中取用(保證了可見性),并不會存在拷貝值的問題吨艇,因此就不會出錯了躬它。

??對于第二種情況:

public class TicketRunnable implements Runnable{
    private int ticket =10;

這里ticket是TicketRunnable類的實例變量,而下面在start()的時候东涡,寫法為:

 TicketRunnable mt = new TicketRunnable();

 Thread t1 = new Thread(mt,"一號窗口");
 Thread t2 = new Thread(mt,"二號窗口");
 Thread t3 = new Thread(mt,"三號窗口");

可到?jīng)]有冯吓,TicketRunnable類只被new了一次,那具體使用的過程中ticket自然也就只有一份了疮跑。如果我們把這里改成:

 TicketRunnable mt1 = new TicketRunnable();
 TicketRunnable mt2 = new TicketRunnable();
 TicketRunnable mt3 = new TicketRunnable();

 Thread t1 = new Thread(mt1,"一號窗口");
 Thread t2 = new Thread(mt2,"二號窗口");
 Thread t3 = new Thread(mt3,"三號窗口");

運行一下组贺,結(jié)果就和第一種情況一樣,每張票被賣了三次祖娘,這是因為失尖,new了三次TicketRunnable,ticket被拷貝了三次渐苏。

那么我們在使用中到底是繼承Thread還是實現(xiàn)Runnable接口呢掀潮?因為Java不支持類的多重繼承,但允許你調(diào)用多個接口整以。所以如果你要繼承其他類胧辽,當(dāng)然是調(diào)用Runnable接口更好了峻仇。一般我們在新建一個線程的時候公黑,直接

    new Thread(new Runnable() {
        @Override
        public void run() {
            //do sth .
        }
    }).start();

就可以了,簡潔明了。

2.線程池

1)Java中創(chuàng)建線程的第三種方式——Callable+FutureTask+ExecutorService

??我們在上面講的創(chuàng)建線程的兩種方式凡蚜,都存在一個缺陷就是:在執(zhí)行完之后人断,無法直接獲取執(zhí)行的結(jié)果。如果需要獲取結(jié)果朝蜘,就需要通過共享變量或者線程間通信的方式來達到效果恶迈,這顯然比較麻煩。而我們現(xiàn)在介紹的Callable+FutureTask的方式谱醇,則能很輕松很隨意的實現(xiàn)結(jié)果的獲取暇仲。

①Callable與Runnable

??Runnable方法我們之前說過他的使用,這里在貼一遍源碼副渴,我們知道奈附,這個run是我們要在程序中手動重寫的,里邊寫的是我們要具體做的事情煮剧,而且這個run方法是void類型的斥滤,也就是說我們執(zhí)行完了之后無法獲取結(jié)果。

public interface Runnable {
    public abstract void run();
}

我們再來看Callable:

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

可以看到這是一個泛型接口勉盅,其中call()方法的返回值就是我們傳進來的泛型V佑颇。而且,這里的call方法和上面的run()方法一樣草娜,也是需要我們在程序中手動重寫的挑胸,其中寫我們具體的要做的事情;不同的是驱还,這里的call方法是需要return的嗜暴。
??Callable一般配合ExecutorService類來使用,我們之后會通過實例展示它的使用:

<T> Future<T> submit(Callable<T> task);
②Future接口與FutureTask類

??首先,F(xiàn)uture是一個接口议蟆,他當(dāng)中封裝了幾個必要的方法:

    //方法用來取消任務(wù)闷沥,如果取消任務(wù)成功則返回true,如果取消任務(wù)失敗則返回false咐容。參數(shù)mayInterruptIfRunning表示
    //是否允許取消正在執(zhí)行卻沒有執(zhí)行完畢的任務(wù)舆逃,如果設(shè)置true,則表示可以取消正在執(zhí)行過程中的任務(wù)戳粒。如果已經(jīng)完成路狮,
    //則無論mayInterruptIfRunning為true還是false,此方法肯定返回false蔚约,即如果取消已經(jīng)完成的任務(wù)會返回false奄妨;
    //如果任務(wù)正在執(zhí)行,若mayInterruptIfRunning設(shè)置為true苹祟,則返回true砸抛,若mayInterruptIfRunning設(shè)置為false评雌,
    //則返回false;如果任務(wù)還沒有執(zhí)行直焙,則無論mayInterruptIfRunning為true還是false景东,肯定返回true。
    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();  //表示任務(wù)是否被取消成功奔誓,如果在任務(wù)正常完成前被取消成功斤吐,則返回 true。
    boolean isDone();   //表示任務(wù)是否已經(jīng)完成厨喂,若任務(wù)完成和措,則返回true;
    V get() throws InterruptedException, ExecutionException;    //方法用來獲取執(zhí)行結(jié)果蜕煌,這個方法會產(chǎn)生阻塞臼婆,會一直等到任務(wù)執(zhí)行完畢才返回;
    V get(long timeout, TimeUnit unit)  //用來獲取執(zhí)行結(jié)果幌绍,如果在指定時間內(nèi)颁褂,還沒獲取到結(jié)果,就直接返回null傀广。
        throws InterruptedException, ExecutionException, TimeoutException;

也就是說Future提供了三種功能:

  • 判斷任務(wù)是否完成颁独;
  • 能夠中斷任務(wù);
  • 能夠獲取任務(wù)執(zhí)行結(jié)果伪冰。

因為Future只是一個接口誓酒,所以是無法直接用來創(chuàng)建對象使用的,因此就有了下面的FutureTask贮聂。

??FutureTask是一個具體類靠柑,實現(xiàn)了RunnableFuture接口,而RunnableFuture接口實現(xiàn)了Runnable和Future<V>接口:

public class FutureTask<V> implements RunnableFuture<V> {
public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

FutureTask類的兩個構(gòu)造器:

public FutureTask(Callable<V> callable) {
public FutureTask(Runnable runnable, V result) {

事實上吓懈,F(xiàn)utureTask是Future接口的唯一實現(xiàn)類歼冰。

③舉個栗子

??使用Callable+FutureTask獲取執(zhí)行結(jié)果:

public class FutureTaskThread {
    public static void main(String[] args) {
        //第一種方式,使用線程池,即ExecutorService
        ExecutorService executor = Executors.newCachedThreadPool();
        Task task = new Task();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
        executor.submit(futureTask);
        executor.shutdown();

        //第二種方式耻警,注意這種方式和第一種方式效果是類似的隔嫡,只不過一個使用的是ExecutorService,一個使用的是Thread
        /*Task task = new Task();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
        Thread thread = new Thread(futureTask);
        thread.start();
        */

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
        System.out.println("主線程:"+Thread.currentThread().getName());

        try {
            System.out.println("task運行結(jié)果"+futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("所有任務(wù)執(zhí)行完畢");
    }
}

class Task implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        System.out.println("子線程:"+Thread.currentThread().getName());
        Thread.sleep(3000);
        int sum = 0;
        for(int i=0;i<100;i++)
            sum += i;
        return sum;
    }
}

打印結(jié)果為:

子線程:pool-1-thread-1
主線程:main
task運行結(jié)果4950
所有任務(wù)執(zhí)行完畢

可以看到甘穿,ExecutorService executor = Executors.newCachedThreadPool();這里我們先從線程池中拿出一個線程腮恩,然后FutureTask<Integer> futureTask = new FutureTask<Integer>(task);新建一個任務(wù),再將這個任務(wù)加入到線程池中去跑executor.submit(futureTask);温兼,跑完之后executor.shutdown();關(guān)閉線程池秸滴,并通過futureTask.get()方法來獲取跑完之后的結(jié)果。從結(jié)果來看募判,打印線程名——子線程:pool-1-thread-1荡含,主線程:main吝羞,顯而易見子線程是在線程池中跑的。
??這里我們需要強調(diào)的一點是内颗,上面的例子中,Callable+FutureTask只是創(chuàng)建一個能夠獲取執(zhí)行結(jié)果的任務(wù)敦腔,真正創(chuàng)建線程的地方是在ExecutorService線程池中均澳。
??如果我們換一種方式,用new Thread來替換線程池符衔,也就是上面注釋掉的第二種方法找前,運行結(jié)果為:

子線程:Thread-0
主線程:main
task運行結(jié)果4950
所有任務(wù)執(zhí)行完畢

可以看到,執(zhí)行結(jié)果完全一樣判族,只不過子線程的線程名是“Thread-0”躺盛,而不是線程池了。這里我們已經(jīng)引入了線程池的概念形帮,那我們接下來就說說線程池的那些事槽惫。

2)Executor框架與線程池

??上面說了創(chuàng)建一般線程的方法,new Thread(new Runnable() {辩撑,這種方法在線程并發(fā)不多的程序中確實不錯界斜,但是如果出現(xiàn)高并發(fā)需要大量創(chuàng)建線程的情況下,勁導(dǎo)致系統(tǒng)的性能變的非常糟糕合冀,主要因為:

  • 線程的創(chuàng)建和銷毀都需要時間各薇,當(dāng)有大量的線程創(chuàng)建和銷毀時,那么這些時間的消耗則比較明顯君躺,將導(dǎo)致性能上的缺失
  • 大量的線程創(chuàng)建峭判、執(zhí)行和銷毀是非常耗cpu和內(nèi)存的,這樣將直接影響系統(tǒng)的吞吐量棕叫,導(dǎo)致性能急劇下降林螃,如果內(nèi)存資源占用的比較多,還很可能造成OOM
  • 大量的線程的創(chuàng)建和銷毀很容易導(dǎo)致GC頻繁的執(zhí)行俺泣,從而發(fā)生內(nèi)存抖動現(xiàn)象治宣,而發(fā)生了內(nèi)存抖動,對于移動端來說砌滞,最大的影響就是造成界面卡頓

??這個時候侮邀,就要用到線程池(ThreadPoolExecutor)了。線程池的基本思想還是一種對象池的思想贝润,開辟一塊內(nèi)存空間绊茧,里面存放了眾多(未死亡)的線程,池中線程執(zhí)行調(diào)度由池管理器來處理打掘。當(dāng)有線程任務(wù)時华畏,從池中取一個鹏秋,執(zhí)行完成后線程對象歸池,這樣可以避免反復(fù)創(chuàng)建線程對象所帶來的性能開銷亡笑,節(jié)省了系統(tǒng)的資源侣夷。

①Executor接口與ExecutorService接口

??首先,這是兩個接口:

public interface Executor {
    void execute(Runnable command);
}
public interface ExecutorService extends Executor {
    void shutdown();
    List<Runnable> shutdownNow();
    boolean isShutdown();
    boolean isTerminated();
    boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException;
    <T> Future<T> submit(Callable<T> task);
    <T> Future<T> submit(Runnable task, T result);
    Future<?> submit(Runnable task);
    ......
}

可以看到仑乌,ExecutorService接口繼承自Executor接口百拓。Executor接口中只定義了一個方法execute(Runnable command),該方法接收一個Runable實例晰甚,它用來執(zhí)行一個任務(wù)衙传。這個任務(wù)就是一個實現(xiàn)了Runnable接口的類。
??ExecutorService繼承自Executor接口厕九,再此基礎(chǔ)之上實現(xiàn)了更加豐富的實現(xiàn)多線程的方法蓖捶,如shutdown(),submit()等扁远。調(diào)用ExecutorService的shutdown()方法來平滑地關(guān)閉 ExecutorService——調(diào)用該方法后俊鱼,將導(dǎo)致ExecutorService停止接受任何新的任務(wù)且等待已經(jīng)提交的任務(wù)執(zhí)行完成(已經(jīng)提交的任務(wù)會分兩類:一類是已經(jīng)在執(zhí)行的,另一類是還沒有開始執(zhí)行的)畅买,當(dāng)所有已經(jīng)提交的任務(wù)執(zhí)行完畢后將會關(guān)閉ExecutorService亭引。因此我們一般用該接口來實現(xiàn)和管理多線程。
??ExecutorService的生命周期包括三種狀態(tài):運行皮获、關(guān)閉焙蚓、終止。創(chuàng)建后便進入運行狀態(tài)洒宝,當(dāng)調(diào)用了shutdown()方法時购公,便進入關(guān)閉狀態(tài),此時意味著ExecutorService不再接受新的任務(wù)雁歌,但它還在執(zhí)行已經(jīng)提交了的任務(wù)宏浩,當(dāng)所有已經(jīng)提交了的任務(wù)執(zhí)行完后,便到達終止?fàn)顟B(tài)靠瞎。如果不調(diào)用shutdown()方法比庄,ExecutorService會一直處在運行狀態(tài),不斷接收新的任務(wù)乏盐,執(zhí)行新的任務(wù)佳窑,服務(wù)器端一般不需要關(guān)閉它,保持一直運行即可父能。

②Executors類與ThreadPoolExecutor類

??首先這是兩個類神凑,注意Executors類Executor接口,多了一個s,不要搞混了。Executors類是一個很單純的類溉委,他沒有實現(xiàn)任何接口鹃唯,也沒有繼承任何父類:

public class Executors {

他的作用是,通過一系列工廠方法用于創(chuàng)建線程池瓣喊,也就是new ThreadPoolExecutor類坡慌,我們可以看下ThreadPoolExecutor類:

public class ThreadPoolExecutor extends AbstractExecutorService {
public abstract class AbstractExecutorService implements ExecutorService {

可以看到,ThreadPoolExecutor繼承自AbstractExecutorService類藻三,但是AbstractExecutorService類實現(xiàn)了ExecutorService接口洪橘,所以相當(dāng)于ThreadPoolExecutor實現(xiàn)了ExecutorService接口。因此趴酣,我們可以通過ExecutorService executor = Executors.newCachedThreadPool();這種方式來創(chuàng)建線程池。
??Executors類中有一下幾種常用的創(chuàng)建線程池的方法:;

    public static ExecutorService newFixedThreadPool(int nThreads)
    創(chuàng)建固定數(shù)目線程的線程池坑夯。

    public static ExecutorService newCachedThreadPool()
        創(chuàng)建一個可緩存的線程池岖寞,調(diào)用execute將重用以前構(gòu)造的線程(如果線程可用)。如果現(xiàn)有線程沒有可用的柜蜈,則創(chuàng)建一個
    新線程并添加到池中仗谆。終止并從緩存中移除那些已有 60 秒鐘未被使用的線程。

    public static ExecutorService newSingleThreadExecutor()
    創(chuàng)建一個單線程化的Executor淑履。

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
    創(chuàng)建一個支持定時及周期性的任務(wù)執(zhí)行的線程池隶垮,多數(shù)情況下可用來替代Timer類。

??一般來說秘噪,CachedTheadPool在程序執(zhí)行過程中通常會創(chuàng)建與所需數(shù)量相同的線程狸吞,然后在它回收舊線程時停止創(chuàng)建新線程,因此它是合理的Executor的首選指煎,只有當(dāng)這種方式會引發(fā)問題時(比如需要大量長時間面向連接的線程時)蹋偏,才需要考慮用FixedThreadPool。(該段話摘自《Thinking in Java》第四版)

線程池內(nèi)部實現(xiàn)原理比較復(fù)雜至壤,我們這里不對其做深究威始,我們目前只需要掌握它的用法:

③Executor執(zhí)行Runnable任務(wù)

??通過Executors的以上四個靜態(tài)工廠方法獲得 ExecutorService實例,而后調(diào)用該實例的execute(Runnable command)方法即可像街。一旦Runnable任務(wù)傳遞到execute()方法黎棠,該方法便會自動在一個線程上執(zhí)行。下面是是Executor執(zhí)行Runnable任務(wù)的示例代碼:

public class CachedThreadPoolRunnable {
     public static void main(String[] args){   
          ExecutorService executorService = Executors.newCachedThreadPool();   
//        ExecutorService executorService = Executors.newFixedThreadPool(5);  
//        ExecutorService executorService = Executors.newSingleThreadExecutor();  
            for (int i = 0; i < 5; i++){   
                executorService.execute(new TestRunnable());   
                System.out.println(" a" + i );   
            }   
            executorService.shutdown();   
      }   
}   
      
class TestRunnable implements Runnable{   
    public void run(){   
        System.out.println(Thread.currentThread().getName() + "線程被調(diào)用了");   
    }   
}

運行結(jié)果為:

 a0
pool-1-thread-1線程被調(diào)用了
 a1
 a2
pool-1-thread-2線程被調(diào)用了
pool-1-thread-1線程被調(diào)用了
 a3
 a4
pool-1-thread-1線程被調(diào)用了
pool-1-thread-3線程被調(diào)用了

可以看到镰绎,pool-1-thread-1這條線程被執(zhí)行了三次脓斩,這說明:①線程池中線程的使用是隨機的,execute會首先在線程池中選擇一個已有空閑線程來執(zhí)行任務(wù)畴栖,如果線程池中沒有空閑線程俭厚,它便會創(chuàng)建一個新的線程來執(zhí)行任務(wù)。②通過Executors.newCachedThreadPool(); 這種方式來創(chuàng)建的線程池是可以緩存其中的線程并重復(fù)利用的驶臊。
??如果我們把上面代碼中Executors.newCachedThreadPool();這種方式換成Executors.newFixedThreadPool(5);這種方式挪挤,得到結(jié)果為:

 a0
pool-1-thread-1線程被調(diào)用了
 a1
 a2
 a3
pool-1-thread-2線程被調(diào)用了
 a4
pool-1-thread-4線程被調(diào)用了
pool-1-thread-3線程被調(diào)用了
pool-1-thread-5線程被調(diào)用了

可以看到叼丑,沒有線程被復(fù)用,全部都是新創(chuàng)建的線程扛门。

??再換成Executors.newSingleThreadExecutor();這種方式鸠信,可以看到谜慌,只有一條線程了:

 a0
 a1
 a2
pool-1-thread-1線程被調(diào)用了
pool-1-thread-1線程被調(diào)用了
pool-1-thread-1線程被調(diào)用了
pool-1-thread-1線程被調(diào)用了
 a3
 a4
pool-1-thread-1線程被調(diào)用了

還有一點扁达,由于上面是通過Runnable這種方式實現(xiàn)的寸宵,因此最后執(zhí)行的結(jié)果不能直接返回又谋,下面我們來看Callable這種方式:

④Executor執(zhí)行Callable任務(wù)

??在Java 5之后靡努,任務(wù)分兩類:一類是實現(xiàn)了Runnable接口的類突琳,一類是實現(xiàn)了Callable接口的類键菱。兩者都可以被ExecutorService執(zhí)行顺饮,但是Runnable任務(wù)沒有返回值火焰,而Callable任務(wù)有返回值劲装。并且Callable的call()方法只能通過ExecutorService的submit(Callable<T> task) 方法來執(zhí)行,并且返回一個 <T>Future<T>昌简,是表示任務(wù)等待完成的 Future占业。
??當(dāng)將一個Callable的對象傳遞給ExecutorService的submit方法,則該call方法自動在一個線程上執(zhí)行纯赎,并且會返回執(zhí)行結(jié)果Future對象,在該Future對象上調(diào)用get方法谦疾,將返回程序執(zhí)行的結(jié)果。同樣犬金,將Runnable的對象傳遞給ExecutorService的submit方法念恍,則該run方法自動在一個線程上執(zhí)行,并且會返回執(zhí)行結(jié)果Future對象晚顷,但是在該Future對象上調(diào)用get方法樊诺,將返回null。

下面給出一個Executor執(zhí)行Callable任務(wù)的示例代碼:

public class ExecutorCallable {
    public static void main(String[] args){   
        ExecutorService executorService = Executors.newCachedThreadPool();   
        List<Future<String>> resultList = new ArrayList<Future<String>>();   
  
        //創(chuàng)建10個任務(wù)并執(zhí)行   
        for (int i = 0; i < 10; i++){   
            //使用ExecutorService執(zhí)行Callable類型的任務(wù)音同,并將結(jié)果保存在future變量中   
            Future<String> future = executorService.submit(new TaskResultCallable(i));   
            //將任務(wù)執(zhí)行結(jié)果存儲到List中   
            resultList.add(future);   
        }   
  
        //遍歷任務(wù)的結(jié)果   
        for (Future<String> fs : resultList){   
                try{   
                    while(!fs.isDone());//Future返回如果沒有完成词爬,則一直循環(huán)等待,直到Future返回完成  
                    System.out.println("任務(wù)返回結(jié)果輸出:"+fs.get());     //打印各個線程(任務(wù))執(zhí)行的結(jié)果   
                }catch(InterruptedException e){   
                    e.printStackTrace();   
                }catch(ExecutionException e){   
                    e.printStackTrace();   
                }finally{   
                    //啟動一次順序關(guān)閉权均,執(zhí)行以前提交的任務(wù)顿膨,但不接受新任務(wù)  
                    executorService.shutdown();   
                }   
        }   
    }   
}   
  
  
class TaskResultCallable implements Callable<String>{   
    private int id;   
  
    public TaskResultCallable(int id){   
        this.id = id;   
    }   
  
    /**  
     * 任務(wù)的具體過程,一旦任務(wù)傳給ExecutorService的submit方法叽赊, 
     * 則該方法自動在一個線程上執(zhí)行 
     */   
    public String call() throws Exception {  
        System.out.println("子線程  :" + Thread.currentThread().getName());   
        //該返回結(jié)果將被Future的get方法得到  
        return "call()方法被自動調(diào)用恋沃,任務(wù)返回的結(jié)果是:" + id + "    " + Thread.currentThread().getName();   
    }   
}

輸出結(jié)果為:

子線程  :pool-1-thread-2
子線程  :pool-1-thread-1
任務(wù)返回結(jié)果輸出:call()方法被自動調(diào)用,任務(wù)返回的結(jié)果是:0    pool-1-thread-1
任務(wù)返回結(jié)果輸出:call()方法被自動調(diào)用必指,任務(wù)返回的結(jié)果是:1    pool-1-thread-2
子線程  :pool-1-thread-3
任務(wù)返回結(jié)果輸出:call()方法被自動調(diào)用囊咏,任務(wù)返回的結(jié)果是:2    pool-1-thread-3
子線程  :pool-1-thread-4
任務(wù)返回結(jié)果輸出:call()方法被自動調(diào)用,任務(wù)返回的結(jié)果是:3    pool-1-thread-4
子線程  :pool-1-thread-5
任務(wù)返回結(jié)果輸出:call()方法被自動調(diào)用,任務(wù)返回的結(jié)果是:4    pool-1-thread-5
子線程  :pool-1-thread-7
子線程  :pool-1-thread-6
任務(wù)返回結(jié)果輸出:call()方法被自動調(diào)用梅割,任務(wù)返回的結(jié)果是:5    pool-1-thread-6
任務(wù)返回結(jié)果輸出:call()方法被自動調(diào)用霜第,任務(wù)返回的結(jié)果是:6    pool-1-thread-7
子線程  :pool-1-thread-8
任務(wù)返回結(jié)果輸出:call()方法被自動調(diào)用,任務(wù)返回的結(jié)果是:7    pool-1-thread-8
子線程  :pool-1-thread-9
任務(wù)返回結(jié)果輸出:call()方法被自動調(diào)用户辞,任務(wù)返回的結(jié)果是:8    pool-1-thread-9
子線程  :pool-1-thread-10
任務(wù)返回結(jié)果輸出:call()方法被自動調(diào)用泌类,任務(wù)返回的結(jié)果是:9    pool-1-thread-10

可以看到,你在callable中的return結(jié)果底燎,就是future.get()中得到的結(jié)果刃榨。

站在巨人的肩膀上摘蘋果:
【Java并發(fā)編程】之十九:并發(fā)新特性—Executor框架與線程池(含代碼)
Java并發(fā)編程:Callable、Future和FutureTask

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末双仍,一起剝皮案震驚了整個濱河市枢希,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌朱沃,老刑警劉巖苞轿,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異为流,居然都是意外死亡呕屎,警方通過查閱死者的電腦和手機让簿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門敬察,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人尔当,你說我怎么就攤上這事莲祸。” “怎么了椭迎?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵锐帜,是天一觀的道長。 經(jīng)常有香客問我畜号,道長缴阎,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任简软,我火速辦了婚禮蛮拔,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘痹升。我一直安慰自己建炫,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布疼蛾。 她就那樣靜靜地躺著肛跌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上衍慎,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天转唉,我揣著相機與錄音,去河邊找鬼西饵。 笑死酝掩,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的眷柔。 我是一名探鬼主播期虾,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼驯嘱!你這毒婦竟也來了镶苞?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤鞠评,失蹤者是張志新(化名)和其女友劉穎茂蚓,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體剃幌,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡聋涨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了负乡。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片牍白。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖抖棘,靈堂內(nèi)的尸體忽然破棺而出茂腥,到底是詐尸還是另有隱情,我是刑警寧澤切省,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布最岗,位于F島的核電站,受9級特大地震影響朝捆,放射性物質(zhì)發(fā)生泄漏般渡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一芙盘、第九天 我趴在偏房一處隱蔽的房頂上張望驯用。 院中可真熱鬧,春花似錦何陆、人聲如沸晨汹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽淘这。三九已至剥扣,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間铝穷,已是汗流浹背钠怯。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留曙聂,地道東北人晦炊。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像宁脊,于是被迫代替她去往敵國和親断国。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 先看幾個概念:線程:進程中負責(zé)程序執(zhí)行的執(zhí)行單元。一個進程中至少有一個線程坐漏。多線程:解決多任務(wù)同時執(zhí)行的需求薄疚,合理...
    yeying12321閱讀 538評論 0 0
  • 下面是我自己收集整理的Java線程相關(guān)的面試題,可以用它來好好準(zhǔn)備面試赊琳。 參考文檔:-《Java核心技術(shù) 卷一》-...
    阿呆變Geek閱讀 14,738評論 14 507
  • 先看幾個概念:線程:進程中負責(zé)程序執(zhí)行的執(zhí)行單元躏筏。一個進程中至少有一個線程板丽。 多線程:解決多任務(wù)同時執(zhí)行的需求,合...
    孫福生微博閱讀 95,532評論 38 314
  • Android Handler機制系列文章整體內(nèi)容如下: Android Handler機制1之ThreadAnd...
    隔壁老李頭閱讀 4,245評論 2 12
  • 生活越美滿碴卧,越能感覺到失去父親的遺憾和傷痛弱卡。為什么老天總是不讓人完美?獨自在新家過夜住册,忽然覺得特別悲傷婶博。 為什么我...
    海魚緣閱讀 198評論 0 4