在程序開發(fā)的實踐當中熏版,為了讓程序表現(xiàn)得更加流暢纷责,我們肯定會需要使用到多線程來提升程序的并發(fā)執(zhí)行性能。但是編寫多線程并發(fā)的代碼一直以來都是一個相對棘手的問題撼短,所以想要獲得更佳的程序性能再膳,我們非常有必要掌握多線程并發(fā)編程的基礎技能。
引入多線程而可能伴隨而來的內(nèi)存問題
雖然使用多線程可以提高程序的并發(fā)量曲横,但是我們需要特別注意因為引入多線程而可能伴隨而來的內(nèi)存問題喂柒。例如:在 Activity 內(nèi)部定義的一個 AsyncTask,它屬于一個內(nèi)部類禾嫉,該類本身和外面的 Activity 是有引用關系的灾杰,如果 Activity 要銷毀的時候,AsyncTask 還仍然在運行夭织,這會導致 Activity 沒有辦法完全釋放吭露,從而引發(fā)內(nèi)存泄漏吠撮。所以說尊惰,多線程是提升程序性能的有效手段之一讲竿,但是使用多線程卻需要十分謹慎小心,如果不了解背后的執(zhí)行機制以及使用的注意事項弄屡,很可能引起嚴重的問題题禀。
下面我們來一個一個分析下系統(tǒng)為我們提供的幾種多線程方式,包括:AsyncTask,HandlerThread,IntentService,ThreadPool
1. AsyncTask
使用場景:
為 UI 線程與工作線程之間進行快速的切換提供一種簡單便捷的機制膀捷。適用于當下立即需要啟動迈嘹,但是異步執(zhí)行的生命周期短暫的使用場景。
基本使用
AsyncTask是一個抽象方法全庸,如果想使用它秀仲,需要先創(chuàng)建一個子類來繼承他,還需要為其指定三個泛型參數(shù):
- Params
在執(zhí)行AsyncTask時需要傳入的參數(shù)壶笼,可用于在后臺任務中使用神僵。 - Progress
后臺任務執(zhí)行時,如果需要在界面上顯示當前的進度覆劈,則使用這里指定的泛型作為進度單位保礼。 - Result
當任務執(zhí)行完畢后,如果需要對結果進行返回责语,則使用這里指定的泛型作為返回值類型炮障。
經(jīng)常需要去重寫的方法有以下四個:
- onPreExecute()
這個方法會在后臺任務開始執(zhí)行之間調(diào)用,用于進行一些界面上的初始化操作坤候,比如顯示一個進度條對話框等胁赢。 - doInBackground(Params...)
這個方法中的所有代碼都會在子線程中運行,我們應該在這里去處理所有的耗時任務白筹。任務一旦完成就可以通過return語句來將任務的執(zhí)行結果進行返回徘键,如果AsyncTask的第三個泛型參數(shù)指定的是Void,就可以不返回任務執(zhí)行結果遍蟋。注意吹害,在這個方法中是不可以進行UI操作的,如果需要更新UI元素虚青,比如說反饋當前任務的執(zhí)行進度它呀,可以調(diào)用publishProgress(Progress...)方法來完成。 - onProgressUpdate(Progress...)
當在后臺任務中調(diào)用了publishProgress(Progress...)方法后棒厘,這個方法就很快會被調(diào)用纵穿,方法中攜帶的參數(shù)就是在后臺任務中傳遞過來的。在這個方法中可以對UI進行操作奢人,利用參數(shù)中的數(shù)值就可以對界面元素進行相應的更新谓媒。 - onPostExecute(Result)
當后臺任務執(zhí)行完畢并通過return語句進行返回時,這個方法就很快會被調(diào)用何乎。返回的數(shù)據(jù)會作為參數(shù)傳遞到此方法中句惯,可以利用返回的數(shù)據(jù)來進行一些UI操作土辩,比如說提醒任務執(zhí)行的結果,以及關閉掉進度條對話框等抢野。
最后在需要的地方調(diào)用方法 new MyAsyncTask().execute(); 就可以運行了拷淘。
注意事項:
首先,默認情況下指孤,所有的 AsyncTask 任務都是被線性調(diào)度執(zhí)行的启涯,他們處在同一個任務隊列當中,按順序逐個執(zhí)行恃轩。假設你按照順序啟動20個 AsyncTask结洼,一旦其中的某個 AsyncTask 執(zhí)行時間過長,隊列中的其他剩余 AsyncTask 都處于阻塞狀態(tài)叉跛,必須等到該任務執(zhí)行完畢之后才能夠有機會執(zhí)行下一個任務补君。為了解決上面提到的線性隊列等待的問題,我們可以使用 AsyncTask.executeOnExecutor()強制指定 AsyncTask 使用線程池并發(fā)調(diào)度任務昧互。
其次挽铁,如何才能夠真正的取消一個 AsyncTask 的執(zhí)行呢?我們知道 AsyncTaks 有提供 cancel()的方法敞掘,但是這個方法實際上做了什么事情呢叽掘?線程本身并不具備中止正在執(zhí)行的代碼的能力,為了能夠讓一個線程更早的被銷毀玖雁,我們需要在 doInBackground()的代碼中不斷的添加程序是否被中止的判斷邏輯更扁。一旦任務被成功中止,AsyncTask 就不會繼續(xù)調(diào)用 onPostExecute()赫冬,而是通過調(diào)用 onCancelled()的回調(diào)方法反饋任務執(zhí)行取消的結果浓镜。我們可以根據(jù)任務回調(diào)到哪個方法(是 onPostExecute 還是 onCancelled)來決定是對 UI 進行正常的更新還是把對應的任務所占用的內(nèi)存進行銷毀等。
最后劲厌,使用 AsyncTask 很容易導致內(nèi)存泄漏膛薛,一旦把 AsyncTask 寫成 Activity 的內(nèi)部類的形式就很容易因為 AsyncTask 生命周期的不確定而導致 Activity 發(fā)生泄漏。
綜上所述补鼻,AsyncTask 雖然提供了一種簡單便捷的異步機制哄啄,但是我們還是很有必要特別關注到他的缺點,避免出現(xiàn)因為使用錯誤而導致的嚴重系統(tǒng)性能問題风范。
2. HandlerThread
使用場景:
為某些回調(diào)方法或者等待某些任務的執(zhí)行設置一個專屬的線程咨跌,并提供線程任務的調(diào)度機制。
大多數(shù)情況下硼婿,AsyncTask 都能夠滿足多線程并發(fā)的場景需要(在工作線程執(zhí)行任務并返回結果到主線程)锌半,但是它并不是萬能的。例如打開相機之后的預覽幀數(shù)據(jù)是通過 onPreviewFrame()的方法進行回調(diào)的寇漫,onPreviewFrame()和 open()相機的方法是執(zhí)行在同一個線程的刊殉。如果使用 AsyncTask殉摔,會因為 AsyncTask 默認的線性執(zhí)行的特性(即使換成并發(fā)執(zhí)行)會導致因為無法把任務及時傳遞給工作線程而導致任務在主線程中被延遲,直到工作線程空閑冗澈,才可以把任務切換到工作線程中進行執(zhí)行钦勘。所以我們需要的是一個執(zhí)行在工作線程陋葡,同時又能夠處理隊列中的復雜任務的功能亚亲,而 HandlerThread 的出現(xiàn)就是為了實現(xiàn)這個功能的,它組合了 Handler腐缤,MessageQueue捌归,Looper 實現(xiàn)了一個長時間運行的線程,不斷的從隊列中獲取任務進行執(zhí)行的功能岭粤。
基本用法:
HandlerThread 繼承于 Thread,它本質(zhì)上是一個線程惜索,只不過是 Android 為我們封裝好了 Looper 和 MessageQueue的線程,簡化了操作剃浇。使用方法很簡單:
// 創(chuàng)建一個線程巾兆,線程名字 : handlerThreadTest
mHandlerThread = new HandlerThread("handlerThreadTest");
mHandlerThread.start();
// Handler 接收消息
final Handler mHandler = new Handler(mHandlerThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
Log.e("Test", "收到 " + msg.obj.toString() + " 在 "
+ Thread.currentThread().getName());
}
};
mTextView = (TextView) findViewById(R.id.text_view);
// 主線程發(fā)出消息
mTextView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Message msg = new Message();
msg.obj = "第一條信息";
mHandler.sendMessage(msg);
Log.e("Test", "發(fā)出 " + msg.obj.toString() + " 在 "
+ Thread.currentThread().getName());
}
});
// 子線程發(fā)出消息
new Thread(new Runnable() {
@Override
public void run() {
Message msg = new Message();
msg.obj = "第二條信息";
mHandler.sendMessage(msg);
Log.e("Test", "發(fā)出 " + msg.obj.toString() + " 在 "
+ Thread.currentThread().getName());
}
}).start();
最后在不需要的時候記得調(diào)用quit();
@Override
protected void onDestroy() {
super.onDestroy();
//停止消息循環(huán)
mHandlerThread.quit();
}
注意事項:
HandlerThread 比較合適處理那些在工作線程執(zhí)行虎囚,需要花費時間偏長的任務角塑。我們只需要把任務發(fā)送給 HandlerThread,然后就只需要等待任務執(zhí)行結束的時候通知返回到主線程就好了淘讥。
另外很重要的一點是圃伶,一旦我們使用了 HandlerThread,需要特別注意給 HandlerThread 設置不同的線程優(yōu)先級蒲列,CPU 會根據(jù)設置的不同線程優(yōu)先級對所有的線程進行調(diào)度優(yōu)化窒朋。
3. IntentSerice
默認的 Service 是執(zhí)行在主線程的,可是通常情況下蝗岖,這很容易影響到程序的繪制性能(搶占了主線程的資源)侥猩。除了前面介紹過的 AsyncTask 與 HandlerThread,我們還可以選擇使用 IntentService 來實現(xiàn)異步操作抵赢。IntentService 繼承自普通 Service 同時又在內(nèi)部創(chuàng)建了一個 HandlerThread拭宁,在 onHandlerIntent()的回調(diào)里面處理扔到 IntentService 的任務,在執(zhí)行完任務后會自動停止瓣俯。所以 IntentService 就不僅僅具備了異步線程的特性杰标,還同時保留了 Service 不受主頁面生命周期影響,優(yōu)先級比較高彩匕,適合執(zhí)行高優(yōu)先級的后臺任務,不容易被殺死的特點腔剂。
使用場景:
適合于執(zhí)行由 UI 觸發(fā)的后臺 Service 任務,并可以把后臺任務執(zhí)行的情況通過一定的機制反饋給 UI驼仪。
基本用法:
public class MyIntentService extends IntentService {
public static UpdateUI updateUI;
/**
* Creates an IntentService. Invoked by your subclass's constructor.
*
* @param name Used to name the worker thread, important only for debugging.
*/
public MyIntentService(String name) {
super(name);
}
public interface UpdateUI{
void updateUI(Message message);
}
public static void setUpdateUI(UpdateUI updateUIInterface){
updateUI = updateUIInterface;
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
// 執(zhí)行耗時操作
Message msg1 = new Message();
msg1.obj ="我是耗時操作";
// 調(diào)用回調(diào) (也可以通過廣播機制完成)
if(updateUI != null){
updateUI.updateUI(msg1);
}
}
}
最后 Activity 通過 Handler 獲取數(shù)據(jù)并刷新UI掸犬。
注意事項:
使用 IntentService 需要特別留意以下幾點:
首先袜漩,因為 IntentService 內(nèi)置的是 HandlerThread 作為異步線程,所以每一個交給 IntentService 的任務都將以隊列的方式逐個被執(zhí)行到湾碎,一旦隊列中有某個任務執(zhí)行時間過長宙攻,那么就會導致后續(xù)的任務都會被延遲處理。
其次介褥,通常使用到 IntentService 的時候座掘,我們會結合使用 BroadcastReceiver 把工作線程的任務執(zhí)行結果返回給主 UI 線程。使用廣播容易引起性能問題柔滔,我們可以使用 LocalBroadcastManager 來發(fā)送只在程序內(nèi)部傳遞的廣播溢陪,從而提升廣播的性能。我們也可以使用 runOnUiThread() 快速回調(diào)到主 UI 線程睛廊。
最后形真,包含正在運行的 IntentService 的程序相比起純粹的后臺程序更不容易被系統(tǒng)殺死,該程序的優(yōu)先級是介于前臺程序與純后臺程序之間的超全。
4. ThreadPool
系統(tǒng)為我們提供了 ThreadPoolExecutor 來實現(xiàn)多線程并發(fā)執(zhí)行任務咆霜。
使用場景:
把任務分解成不同的單元,分發(fā)到各個不同的線程上嘶朱,進行同時并發(fā)處理蛾坯。
基本用法:
線程池有四個構造方法,這四個構造方法咋一看见咒,前面三個都是調(diào)用第四個構造方法實現(xiàn)的偿衰,每一個構造方法都特別復雜,參數(shù)很多改览,使用起來比較麻煩下翎。
下面列出是四種構造方法,從簡單到復雜:
- 第一種
ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue
);
- 第二種
ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler
);
- 第三種
ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory
);
- 第四種
ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
);
參數(shù)作用:
corePoolSize:
池中所保存的線程數(shù)宝当,包括空閑線程视事。maximumPoolSize:
池中允許的最大線程數(shù)。keepAliveTime:
當線程數(shù)大于核心時庆揩,此為終止前多余的空閑線程等待新任務的最長時間俐东。unit:
keepAliveTime參數(shù)的時間單位workQueue:
執(zhí)行前用于保持任務的隊列。此隊列僅保持由 execute 方法提交的 Runnable 任務订晌。threadFactory
執(zhí)行程序創(chuàng)建新線程時使用的工廠虏辫。**handler **
由于超出線程范圍和隊列容量而使執(zhí)行被阻塞時所使用的處理程序。
這里用復雜的一個構造方法說明如何手動創(chuàng)建一個線程池锈拨。
package com.example.thread.threadpool;
import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @auther MaxLiu
* @time 2017/2/23
*/
public class ThreadPoolTest {
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int BLOCK_POOL_SIZE = 2;
private static final int ALIVE_POOL_SIZE = 2;
private static ThreadPoolExecutor executor;
public static void main(String args[]) {
executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,// 核心線程數(shù) 最小
MAX_POOL_SIZE,// 最大執(zhí)行線程數(shù)
ALIVE_POOL_SIZE,// 空閑線程超時
TimeUnit.SECONDS,// 超時時間單位
// 當線程池達到corePoolSize時砌庄,新提交任務將被放入workQueue中,
// 等待線程池中任務調(diào)度執(zhí)行
new ArrayBlockingQueue<Runnable>(BLOCK_POOL_SIZE),// 阻塞隊列大小
// 線程工廠,為線程池提供創(chuàng)建新線程的功能娄昆,它是一個接口佩微,
// 只有一個方法:Thread newThread(Runnable r)
Executors.defaultThreadFactory(),
// 線程池對拒絕任務的處理策略。一般是隊列已滿或者無法成功執(zhí)行任務萌焰,
// 這時ThreadPoolExecutor會調(diào)用handler的rejectedExecution
// 方法來通知調(diào)用者
new ThreadPoolExecutor.AbortPolicy()
);
executor.allowCoreThreadTimeOut(true);
/*
* ThreadPoolExecutor默認有四個拒絕策略:
*
* 1哺眯、ThreadPoolExecutor.AbortPolicy() 直接拋出異常RejectedExecutionException
* 2、ThreadPoolExecutor.CallerRunsPolicy() 直接調(diào)用run方法并且阻塞執(zhí)行
* 3扒俯、ThreadPoolExecutor.DiscardPolicy() 直接丟棄后來的任務
* 4奶卓、ThreadPoolExecutor.DiscardOldestPolicy() 丟棄在隊列中隊首的任務
*/
for (int i = 0; i < 10; i++) {
try {
executor.execute(new WorkerThread("線程 --> " + i));
LOG();
} catch (Exception e) {
System.out.println("AbortPolicy...");
}
}
executor.shutdown();
// 所有任務執(zhí)行完畢后再次打印日志
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println("\n\n---------執(zhí)行完畢---------\n");
LOG();
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
/**
* 打印 Log 信息
*/
private static void LOG() {
System.out.println(" ==============線程池===============\n"
+ " 線程池中線程數(shù) : " + executor.getPoolSize()
+ " 等待執(zhí)行線程數(shù) : " + executor.getQueue().size()
+ " 所有的任務數(shù) : " + executor.getTaskCount()
+ " 執(zhí)行任務的線程數(shù) : " + executor.getActiveCount()
+ " 執(zhí)行完畢的任務數(shù) : " + executor.getCompletedTaskCount()
);
}
// 模擬線程任務
public static class WorkerThread implements Runnable {
private String threadName;
public WorkerThread(String threadName) {
this.threadName = threadName;
}
@Override
public synchronized void run() {
int i = 0;
boolean flag = true;
try {
while (flag) {
i++;
if (i > 2) flag = false;
}
} catch (Exception e) {
e.printStackTrace();
}
}
public String getThreadName() {
return threadName;
}
}
}
結果:
注意事項:
- 使用線程池需要特別注意同時并發(fā)線程數(shù)量的控制,理論上來說陵珍,我們可以設置任意你想要的并發(fā)數(shù)量寝杖,但是這樣做非常的不好违施。因為 CPU 只能同時執(zhí)行固定數(shù)量的線程數(shù)互纯,一旦同時并發(fā)的線程數(shù)量超過 CPU 能夠同時執(zhí)行的閾值,CPU 就需要花費精力來判斷到底哪些線程的優(yōu)先級比較高磕蒲,需要在不同的線程之間進行調(diào)度切換留潦。一旦同時并發(fā)的線程數(shù)量達到一定的量級,這個時候 CPU 在不同線程之間進行調(diào)度的時間就可能過長辣往,反而導致性能嚴重下降兔院。
- 另外需要關注的一點是,每開一個新的線程站削,都會耗費至少 64K+ 的內(nèi)存坊萝。為了能夠方便的對線程數(shù)量進行控制,ThreadPoolExecutor 為我們提供了初始化的并發(fā)線程數(shù)量许起,以及最大的并發(fā)數(shù)量進行設置十偶。
- 另外需要關注的一個問題是:Runtime.getRuntime().availableProcesser()方法并不可靠,他返回的值并不是真實的 CPU 核心數(shù)园细,因為 CPU 會在某些情況下選擇對部分核心進行睡眠處理惦积,在這種情況下,返回的數(shù)量就只能是激活的 CPU 核心數(shù)猛频。