今天浦旱,我們細(xì)說Android下的多線程

楔子

蘇格拉底曾說過:“學(xué)會了多線程,你就學(xué)會了壓榨CPU九杂,就好像資本家對無產(chǎn)階級做的那事一樣颁湖。”

多線程是開發(fā)人員必不可少的技術(shù)點例隆,也是初學(xué)者不太容易掌握好的一個難點甥捺。要想設(shè)計出優(yōu)秀的程序,那必然需要合理的線程調(diào)度镀层。今天鄙人就給大家細(xì)說下Android中與多線程相關(guān)的知識點镰禾,揭開多線程神秘的面紗。

本篇文章僅介紹多線程的各種實現(xiàn)方式,不過多涉及深入的基礎(chǔ)原理探究吴侦,達(dá)到“所見即所學(xué)谷饿,所學(xué)即可用”的效果。關(guān)于各種多線程原理的深入探究妈倔,有機(jī)會放在后面的專欄逐一介紹。

多線程是什么绸贡?我為什么要用多線程盯蝴?

線程和進(jìn)程的概念

按照操作系統(tǒng)中的描述,線程是CPU調(diào)度的最小單元听怕,同時線程是一種有限的系統(tǒng)資源捧挺。而進(jìn)程一般指一個執(zhí)行單元,在PC和移動設(shè)備上指一個程序或者一個應(yīng)用尿瞭。一個進(jìn)程可以包含多個線程闽烙。

簡單點理解,一個Android APP就是一個進(jìn)程声搁,一個APP里面有多個線程黑竞,我們多線程編程的意義就是實現(xiàn)“一個APP多個線程”。

有杠精可能會問疏旨,那我可不可以一個APP多個進(jìn)程很魂?又可不可以一個進(jìn)程只有一個線程?

我告訴你檐涝,可以遏匆,都可以。

單線程的APP只包括Android的UI線程也是能運(yùn)行的谁榜;一個APP多個進(jìn)程也是可以達(dá)到的幅聘,實現(xiàn)方式涉及到Android的IPC機(jī)制,這里不細(xì)說窃植。

為什么要使用多線程帝蒿?

這里杠精可能會說,那你單線程也能跑撕瞧,我為啥還要整多線程陵叽?

我告訴你,首先這句話從Android開發(fā)的角度來講丛版,近似于一個假命題巩掺。因為谷歌爸爸現(xiàn)在強(qiáng)制規(guī)定了不能在UI線程進(jìn)行耗時操作,必須放到子線程里面去页畦,除非你的程序不涉及耗時操作胖替。究其原因,是因為在UI線程進(jìn)行耗時操作的話,給用戶的使用體驗就是界面“卡頓”独令。同時端朵,如果UI線程被阻塞超過一定時間會觸發(fā)ANR(Application Not Responding)錯誤键耕。

從底層的角度來講娜搂,多線程可以使得整個環(huán)境能夠異步執(zhí)行鹊奖,這有助于防止浪費CPU時鐘周期從而提高效率停忿。換言之杠氢,多線程能更充分的利用CPU資源贤惯,從而提高程序的運(yùn)行效率老速。

那我怎么進(jìn)行多線程編程啊研?

Thread類和Runnable接口

要想定義一個線程只需要新建一個類繼承自Thread裙戏,然后重寫父類的run方法即可

class MyThread extends Thread {
    @Override
    public void run() {
        doSomething();
    }
}

//在需要的時候啟動線程
new MyThread().start();

優(yōu)化一下乘凸?

我們可以沒必要繼承整個Thread類,只實現(xiàn)Runnable接口就好了

class MyThread implements Runnable {
    @Override
    public void run() {
        doSomething()
    }
}

//啟動線程
MyThread myThread = new MyThread();
new Thread(myThread).start();

那我不想專門再寫一個線程類怎么辦累榜?可以使用匿名類

new Thread(new Runnable() {
    @Override
    public void run() {
        doSomething();
    }
}).start();

線程池

線程池的意義

既然我都會用Runnable接口來創(chuàng)建線程了营勤,還要線程池干啥?其實不然壹罚,隨意創(chuàng)建線程的操作在實際開發(fā)中是極為不推薦的葛作。為啥?因為線程也是一種資源渔嚷,反復(fù)的創(chuàng)建和銷毀線程會帶來一定性能上的額外開銷进鸠。與其相比,線程池主要有以下幾個優(yōu)點:

  • 重用線程池中的線程形病,避免因為線程的創(chuàng)建和銷毀所帶來的性能開銷
  • 能有效控制線程池的最大并發(fā)數(shù)客年,避免大量的線程之間因相互搶占系統(tǒng)資源而導(dǎo)致的阻塞現(xiàn)象
  • 能夠?qū)€程進(jìn)行簡單的管理,并提供定時執(zhí)行以及指定間隔循環(huán)執(zhí)行等功能

線程池的結(jié)構(gòu)和原理

一個完整的線程池應(yīng)該有這么幾個組成部分

  • 核心線程
  • 任務(wù)隊列
  • 非核心線程

當(dāng)我們通過線程池執(zhí)行異步任務(wù)的時候漠吻,其實是依次進(jìn)行了下面的流程

  1. 檢查核心線程數(shù)是否到達(dá)最大值量瓜,否則創(chuàng)建新的核心線程執(zhí)行任務(wù),是則進(jìn)行下一步
  2. 檢查任務(wù)隊列是否已滿途乃,否則將任務(wù)添加到任務(wù)隊列中绍傲,是則進(jìn)行下一步
  3. 檢查非核心線程數(shù)是否到達(dá)最大值,否則創(chuàng)建新的非核心線程執(zhí)行任務(wù)耍共,是則說明這個線程池已經(jīng)飽和了烫饼,執(zhí)行飽和策略。默認(rèn)的飽和策略是拋出RejectedExecutionException異常

下面手搓一個線程池的實現(xiàn)

//CPU核心數(shù)
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
//核心線程數(shù)
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
//最大線程數(shù)
private static final int MAX_POOL_SIZE = CPU_COUNT * 2 + 1;
//非核心線程閑置的超時時間
private static final int KEEP_ALIVE_TIME = 1;
//任務(wù)隊列
private static final BlockingQueue<Runnable> sPoolWorkQueue =
        new LinkedBlockingQueue<Runnable>(128);
//線程池
private ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE,
        MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, sPoolWorkQueue);

private void fun(){
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            //子線程處理耗時操作
            doSomething();
        }
    };
    poolExecutor.execute(runnable);
}

這樣我們就實現(xiàn)了一個簡單的線程池试读,核心線程數(shù)為CPU數(shù)量+1杠纵,非核心線程數(shù)為CPU數(shù)量*2+1,非核心線程的閑置時間為1秒钩骇,任務(wù)隊列的大小為128比藻。

線程池還有具體的好幾種分類和相應(yīng)不同的實現(xiàn)方式铝量,這里不再細(xì)說。

Handler

有朋友可能會說银亲,你講的這些都是Java多線程里面的東西慢叨,能不能整點咱Android特有的?OK务蝠,現(xiàn)在進(jìn)入專業(yè)時間拍谐。

Handler是Android提供的一種異步消息處理機(jī)制,要學(xué)會使用Handler我們首先來了解下消息處理四兄弟:

  • Message
  • Handler
  • MessageQueue
  • Looper

Handler可以幫助我們實現(xiàn)在不同的線程之間傳遞消息馏段,這里的Message就是消息本體赠尾,也就是我們想要傳遞的那個東西。

Handler在這里扮演的角色是消息處理者毅弧,它的主要作用是發(fā)送和處理消息。

MessageQueue是一個消息隊列当窗,Handler發(fā)送過來的消息會放在這個隊列里面够坐,每個線程只會有一個MessageQueue對象。

Looper是線程中消息隊列的管家崖面,它會無限循環(huán)運(yùn)行元咙,每發(fā)現(xiàn)MessageQueue中存在一條消息,它就會把消息取出然后發(fā)送給Handler巫员。每一個線程也只能有一個Looper對象庶香。

好了,基本原理已經(jīng)了解简识,現(xiàn)在我們來反手搓一個Handler

private static final int FLAG = 1;

private Handler mHandler = new Handler(){
    @Override
    public void handleMessage(@NonNull Message msg) {
        if (FLAG == msg.what){
            //這里已經(jīng)回到主線程了
            doSomething();
        }
    }
};

private void fun(){
    new Thread(new Runnable() {
        @Override
        public void run() {
            //子線程發(fā)送消息
            Message message = new Message();
            message.what = FLAG;
            mHandler.sendMessage(message);
        }
    }).start();
}

AsyncTask

除了Handler以外赶掖,谷歌爸爸還給我們提供AsyncTask來進(jìn)行線程的切換。AsyncTask是一種輕量級的異步任務(wù)七扰,它可以在線程池中執(zhí)行后臺任務(wù)奢赂,然后把執(zhí)行的進(jìn)度和最終結(jié)果傳遞給主線程。從實現(xiàn)原理上來講颈走,AsyncTask是對Thread和Handle的再次封裝膳灶。

AsyncTask本身是一個抽象的泛型類,有四個親兒子:

  • onPreExecute()
  • doInBackground(Params...params)
  • onProgressUpdate(Progress...values)
  • onPostExecute(Result result)

最先執(zhí)行的是方法是onPreExecute()方法立由,位于主線程中轧钓,一般用來做一些準(zhǔn)備工作。

然后執(zhí)行doInBackground()方法锐膜,位于線程池中毕箍,用來執(zhí)行異步任務(wù),params表示異步任務(wù)的輸入?yún)?shù)枣耀。這個方法需要返回結(jié)果給onPostExecute()方法霉晕。

onProgressUpdate()方法在主線程中執(zhí)行庭再,當(dāng)后臺任務(wù)的執(zhí)行進(jìn)度發(fā)生變化時這個方法會被調(diào)用。

onPostExecute()方法在最后異步任務(wù)完成之后會被調(diào)用牺堰,位于主線程中拄轻,result參數(shù)是后臺任務(wù)的返回值,即doInBackground()的返回值伟葫。

OK恨搓,基本原理已經(jīng)了解了,現(xiàn)在我們來手搓一個AsyncTask

class DownloadTask extends AsyncTask<Void,Integer,Boolean> {

    @Override
    protected void onPreExecute() {
        //這里我們使用了一個顯示進(jìn)度的Dialog筏养,具體實現(xiàn)不表
        progressDialog.show();
    }

    @Override
    protected Boolean doInBackground(Void... voids) {
        try {
            while (true){
                //調(diào)用我們的doDownload下載方法斧抱,具體實現(xiàn)不表
                int downloadPercent = doDownload();
                //使用publishProgress方法來更新執(zhí)行的進(jìn)度
                publishProgress(downloadPercent);
                if (downloadPercent >= 100)
                    break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return true;
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        //更新下載進(jìn)度
        progressDialog.setMessage("Download "+values[0]+"%");
    }

    @Override
    protected void onPostExecute(Boolean aBoolean) {
        //下載完成
        progressDialog.dismiss();
    }
}

這里我們創(chuàng)建了一個Download類繼承自AsyncTask,有三個泛型渐溶,void表示不需要給后臺任務(wù)傳入?yún)?shù)辉浦,Integer表示用整數(shù)類型來作為進(jìn)度顯示的單位,Boolean表示用布爾類型來反饋后臺任務(wù)的執(zhí)行結(jié)果茎辐。

要讓我們的這個AsyncTask跑起來也很簡單宪郊,只需要執(zhí)行:

new DownloadTask().execute();

IntentService

IntentService是一種特殊的Service,它繼承了Service并且是一個抽象類拖陆,我們可以創(chuàng)建它的子類來使用弛槐。IntentService也可以用于執(zhí)行后臺的耗時任務(wù),并且當(dāng)任務(wù)執(zhí)行完畢之后它會自動停止依啰。

IntentService因為是服務(wù)的原因乎串,所以和單純的線程相比它的優(yōu)先級要高很多,從而更不容易被系統(tǒng)殺死速警。

IntentService的內(nèi)部實現(xiàn)是封裝了HandlerThread和Handler叹誉,使用的話要遵循Service的使用方法,這里先略過后面有機(jī)會在Service的專欄里面再詳細(xì)介紹闷旧。

RxJava

有杠精可能會說桂对,你講的這些方法,一個比一個長鸠匀,一個比一個復(fù)雜蕉斜,就不能整個簡單又粗暴的東西?

這個時候就需要祭出神兵利器RxJava了缀棍。

RxJava又是個啥宅此?

其實網(wǎng)絡(luò)上RxJava的入門文章多如過江之鯽,這里不打算過多的深入介紹爬范。RxJava是一種響應(yīng)式編程父腕,大家不是很明白的話可以粗暴的理解為更優(yōu)雅的多線程實現(xiàn)即可。

那怎么操作RxJava青瀑?

先手搓一個RxJava的普通實現(xiàn)方式

private void fun(){
    Observable<Integer> observable = Observable.create(new ObservableOnSubscribe<Integer>() {
        @Override
        public void subscribe(ObservableEmitter<Integer> emitter) throws Exception {
            emitter.onNext(1);
        }
    });

    observable.subscribeOn(Schedulers.io())     //表示在io線程執(zhí)行訂閱
            .observeOn(AndroidSchedulers.mainThread())  //表示在主線程接收訂閱
            .subscribe(new Observer<Integer>() {
                @Override
                public void onSubscribe(Disposable d) {
                    //接收訂閱之前調(diào)用
                }

                @Override
                public void onNext(Integer integer) {
                    //接收訂閱成功調(diào)用
                    doSomething();
                }

                @Override
                public void onError(Throwable e) {
                    //接收訂閱出錯調(diào)用
                }

                @Override
                public void onComplete() {
                    //接收訂閱完成調(diào)用
                }
            });
}

emmmmm看起來好像還是挺復(fù)雜的啊璧亮,能不能再整簡單點萧诫?

OK,鏈?zhǔn)秸{(diào)用加lambda安排上

private void fun() {
    Observable.create(emitter -> emitter.onNext(1))
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(integer -> {
                //接收訂閱成功
                doSomething();
            }, throwable -> {});
}

嗯......有內(nèi)味了枝嘶。

這串代碼我們是發(fā)送了一個Integer類型的數(shù)據(jù)帘饶;

subscribeOn()指定了我們發(fā)送的線程是在后臺的io線程,就可以理解為一個子線程群扶;

observeOn指定了我們接收的線程為主線程及刻;

subscribe只接收成功的消息,相當(dāng)于上面的OnNext()方法竞阐,本質(zhì)上是我們在這里創(chuàng)建了一個Comsumer對象來接收缴饭;

throwable在接收失敗的時候調(diào)用,相當(dāng)于上面的onError()方法骆莹。

RxKotlin

RxKotlin可以理解為RxJava在Kotlin上的一個變種颗搂,原理都是一樣的,只是操作語言變成了Kotlin幕垦,然后封裝了一下使得可以更優(yōu)雅的調(diào)用峭火,這里給大家一個具體的實現(xiàn)案例,不再過多講解智嚷。

private fun test() {
    Observable.create<Int> { 1 }
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeBy(
                    onNext = {},
                    onError = {}
            )
}

Kotlin協(xié)程

協(xié)程其實和上面所說的線程并不是一個概念,協(xié)程是什么纺且?根據(jù)官方文檔的描述盏道,協(xié)程本質(zhì)上是輕量級的線程。既然是輕量载碌,那說明協(xié)程的資源消耗和性能等方面和線程比起來應(yīng)該是有優(yōu)勢的猜嘱。那這樣看來我們以前使用多線程實現(xiàn)的異步功能,現(xiàn)在基本上都可以用協(xié)程來替代了嫁艇。

協(xié)程是一個全新的東西朗伶,介于篇幅這里就不展開講解了,后面會專門寫介紹協(xié)程的文章步咪。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末论皆,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子猾漫,更是在濱河造成了極大的恐慌点晴,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件悯周,死亡現(xiàn)場離奇詭異粒督,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)禽翼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進(jìn)店門屠橄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來族跛,“玉大人,你說我怎么就攤上這事锐墙〗负澹” “怎么了?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵贮匕,是天一觀的道長姐仅。 經(jīng)常有香客問我,道長刻盐,這世上最難降的妖魔是什么掏膏? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮敦锌,結(jié)果婚禮上馒疹,老公的妹妹穿的比我還像新娘。我一直安慰自己乙墙,他們只是感情好颖变,可當(dāng)我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著听想,像睡著了一般腥刹。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上汉买,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天衔峰,我揣著相機(jī)與錄音,去河邊找鬼蛙粘。 笑死垫卤,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的出牧。 我是一名探鬼主播穴肘,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼舔痕!你這毒婦竟也來了评抚?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤伯复,失蹤者是張志新(化名)和其女友劉穎盈咳,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體边翼,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡鱼响,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了组底。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片丈积。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡筐骇,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出江滨,到底是詐尸還是另有隱情铛纬,我是刑警寧澤,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布唬滑,位于F島的核電站告唆,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏晶密。R本人自食惡果不足惜擒悬,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望稻艰。 院中可真熱鬧懂牧,春花似錦、人聲如沸尊勿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽元扔。三九已至躯保,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間澎语,已是汗流浹背途事。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留咏连,地道東北人。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓鲁森,卻偏偏與公主長得像祟滴,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子歌溉,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,435評論 2 359