Android多線程
多線程開發在Android技術中非常重要,能否熟練掌握這些技術是衡量一個工程師技術水平能力的一個重要標準,也是決定能否開發出高效優質應用的前提條件。下面將分別展開描述以及對比,並結合實際工作場合分析優劣。主要有以下幾種:
- Thread
- Handler
- HandlerThread
- IntentService
- ThreadPool
1 Thread(線程)
1.1 定義
一個基本的CPU執行單元 & 程序執行流的最小單元
組成:線程ID + 程序計數器 + 寄存器集合 + 堆棧。比進程更小的可獨立運行的基本單位,可理解爲:輕量級進程。線程自己不擁有系統資源,與其他線程共享進程所擁有的全部資源。
1.2 與進程的區別
- 進程是資源分配的最小單位,線程是程序執行的最小單位。
- 進程有自己的獨立地址空間,每啓動一個進程,系統就會爲它分配地址空間,建立數據表來維護代碼段、堆棧段和數據段,這種操作非常昂貴。而線程是共享進程中的數據的,使用相同的地址空間,因此CPU切換一個線程的花費遠比進程要小很多,同時創建一個線程的開銷也比進程要小很多。
- 線程之間的通信更方便,同一進程下的線程共享全局變量、靜態變量等數據,而進程之間的通信需要以通信的方式(IPC)進行。不過如何處理好同步與互斥是編寫多線程程序的難點。
- 但是多進程程序更健壯,多線程程序只要有一個線程死掉,整個進程也死掉了,而一個進程死掉並不會對另外一個進程造成影響,因爲進程有自己獨立的地址空間。
1.3 線程分類
線程主要分爲:守護線程、非守護線程(用戶線程)
1.3.1 守護線程
- 定義:守護用戶線程的線程,即在程序運行時爲其他線程提供一種通用服務
- 常見:如 垃圾回收線程
- 設置方式:
//設置該線程爲守護線程
thread.setDaemon(true);
1.3.2 非守護線程(用戶線程)
主要包括:主線程 、子線程(工作線程)。
a. 主線程(UI線程)
- 定義:Android系統在程序啓動時會自動啓動一條主線程
- 作用:處理四大組件與用戶進行交互的事情(如UI、界面交互相關)
- 注:因爲用戶隨時會與界面發生交互,因此主線程任何時候都必須保持很高的響應速度,所以主線程不允許進行耗時操作,否則會出現ANR
b. 子線程(工作線程)
- 定義:手動創建的線程
- 作用:耗時的操作(網絡請求、I/O操作等)
1.3.3 守護線程 與 非守護線程的區別
- 區別:虛擬機是否已退出:
- 當所有用戶線程結束時,因爲沒有守護的必要,所以守護線程也會終止,虛擬機也同樣退出;
- 反過來,只要任何用戶線程還在運行,守護線程就不會終止,虛擬機就不會退出
1.3 線程優先級
1.3.1 表示
線程優先級分爲10個級別,分別用Thread類常量表示。
// 譬如:
Thread.MIN_PRIORITY // 優先級1
Thread.MAX_PRIORITY // 優先級10
1.3.2 設置
通過方法setPriority(int grade)進行優先級設置
默認線程優先級是5,即 Thread.NORM_PRIORITY
1.4 多線程 - 介紹
多個線程同時進行,即多個任務同時進行
其實,計算機任何特定時刻只能執行一個任務;
多線程只是一種錯覺:只是因爲JVM快速調度資源來輪換線程,使得線程不斷輪流執行,所以看起來好像在同時執行多個任務而已
1.5 線程調度
1.5.1 調度方式
- 當系統存在大量線程時,系統會通過時間片輪轉的方式調度線程,因此線程不可能做到絕對的併發
- 處於就緒狀態(Runnable)的線程都會進入到線程隊列中等待CPU資源
同一時刻在線程隊列中可能有很多個
在採用時間片的系統中,每個線程都有機會獲得CPU的資源以便進行自身的線程操作;當線程使用CPU資源的時間到後,即時線程沒有完成自己的全部操作,JVM也會中斷當前線程的執行,把CPU資源的使用權切換給下一個隊列中等待的線程。
被中斷的線程將等待CPU資源的下一次輪迴,然後從中斷處繼續執行
1.5.2 調度優先級
Java虛擬機(JVM)中的線程調度器負責管理線程,並根據以下規則進行調度:
- 根據線程優先級(高-低),將CPU資源分配給各線程
- 具備相同優先級的線程以輪流的方式獲取CPU資源
- 示例
存在A、B、C、D四個線程,其中:A和B的優先級高於C和D(A、B同級,C、D同級,那麼JVM將先以輪流的方式調度A、B,直到A、B線程死亡,再以輪流的方式調度C、D
1.6 線程的使用
- 繼承Thread類
// 步驟1:創建線程類 (繼承自Thread類)
class MyThread extends Thread{
// 步驟2:複寫run(),內容 = 定義線程行爲
@Override
public void run(){
... // 定義的線程行爲
}
}
// 步驟3:創建線程對象,即 實例化線程類
MyThread mt=new MyThread(“線程名稱”);
// 步驟4:通過 線程對象 控制線程的狀態,如 運行、睡眠、掛起 / 停止
// 此處採用 start()開啓線程
mt.start();
- 配合Runnable接口創建線程
// 步驟1:創建線程輔助類,實現Runnable接口
class MyThread implements Runnable{
....
@Override
// 步驟2:複寫run(),定義線程行爲
public void run(){
}
}
// 步驟3:創建線程輔助對象,即 實例化 線程輔助類
MyThread mt = new MyThread();
// 步驟4:創建線程對象,即 實例化線程類;線程類 = Thread類;
// 創建時通過Thread類的構造函數傳入線程輔助類對象
// 原因:Runnable接口並沒有任何對線程的支持,我們必須創建線程類(Thread類)的實例,從Thread類的一個實例內部運行
Thread td=new Thread(mt);
// 步驟5:通過 線程對象 控制線程的狀態,如 運行、睡眠、掛起 / 停止
// 當調用start()方法時,線程對象會自動回調線程輔助類對象的run(),從而實現線程操作
td.start();
- 匿名類創建線程
new Thread(): 在阿里開發手冊中明確禁止使用這種方式開啓新線程,主要是因爲新線程這樣開啓之後無法主動停止,只適合執行耗時短的輕量級任務
// 步驟1:採用匿名類,直接 創建 線程類的實例
new Thread("線程名稱") {
// 步驟2:複寫run(),內容 = 定義線程行爲
@Override
public void run() {
// 步驟3:通過 線程對象 控制線程的狀態,如 運行、睡眠、掛起 / 停止
}
}.start();
1.7 new Thread()的缺點
在實際多線程開發中,一般不建議直接用new Thread(),性能開銷達,不好管理,而要使用下面即將提到的其他多線程技術。
- 每次new Thread()耗費性能
- 調用new Thread()創建的線程缺乏管理,被稱爲野線程,而且可以無限制創建,之間相互競爭,會導致過多佔用系統資源導致系統癱瘓。
- 不利於擴展,比如如定時執行、定期執行、線程中斷
2 Handler
2.1 定義
一套 Android 消息傳遞機制
2.2 作用
在多線程的應用場景中,將工作線程中需更新UI的操作信息 傳遞到 UI主線程,從而實現 工作線程對UI的更新處理,最終實現異步消息的處理
2.3 意義
多個線程併發更新UI的同時 保證線程安全
2.4 相關概念
Handler 、 Looper 、Message 這三者都與Android異步消息處理線程相關的概念。那麼什麼叫異步消息處理線程呢?
- 異步消息處理線程啓動後會進入一個無限的循環體之中,每循環一次,從其內部的消息隊列中取出一個消息,然後回調相應的消息處理函數,執行完成一個消息後則繼續循環。若消息隊列爲空,線程則會阻塞等待。
- 那麼Android消息機制主要是指Handler的運行機制,Handler運行需要底層的MessageQueue和Looper支撐。其中MessageQueue採用的是單鏈表的結構,Looper可以叫做消息循環。由於MessageQueue只是一個消息存儲單元,不能去處理消息,而Looper就是專門來處理消息的,Looper會以無限循環的形式去查找是否有新消息,如果有的話,就處理,否則就一直等待着。
- 我們知道,Handler創建的時候會採用當前線程的Looper來構造消息循環系統,需要注意的是,線程默認是沒有Looper的,如果需要使用Handler就必須爲線程創建Looper,因爲默認的UI主線程,也就是ActivityThread,ActivityThread被創建的時候就會初始化Looper,這也是在主線程中默認可以使用Handler的原因。如果想讓該線程具有消息隊列和消息循環,並具有消息處理機制,就需要在線程中首先調用Looper.prepare()來創建消息隊列,然後調用Looper.loop()進入消息循環。如以下代碼所示:
public class LopperThread extends Thread{
public Handler mHandler;
@Override
public void run(){
Looper.prepare();
mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//處理消息隊列
}
};
Looper.loop();
}
}
2.5 使用方式
- Handler.sendMessage
在該使用方式中,又分爲2種:新建Handler子類(內部類)、匿名 Handler子類
/**
* 方式1:新建Handler子類(內部類)
*/
// 步驟1:自定義Handler子類(繼承Handler類) & 複寫handleMessage()方法
class mHandler extends Handler {
// 通過複寫handlerMessage() 從而確定更新UI的操作
@Override
public void handleMessage(Message msg) {
...// 需執行的UI操作
}
}
// 步驟2:在主線程中創建Handler實例
private Handler mhandler = new mHandler();
// 步驟3:創建所需的消息對象
Message msg = Message.obtain(); // 實例化消息對象
msg.what = 1; // 消息標識
msg.obj = "AA"; // 消息內容存放
// 步驟4:在工作線程中 通過Handler發送消息到消息隊列中
// 可通過sendMessage() / post()
// 多線程可採用AsyncTask、繼承Thread類、實現Runnable
mHandler.sendMessage(msg);
// 步驟5:開啓工作線程(同時啓動了Handler)
// 多線程可採用AsyncTask、繼承Thread類、實現Runnable
/**
* 方式2:匿名內部類
*/
// 步驟1:在主線程中 通過匿名內部類 創建Handler類對象
private Handler mhandler = new Handler(){
// 通過複寫handlerMessage()從而確定更新UI的操作
@Override
public void handleMessage(Message msg) {
...// 需執行的UI操作
}
};
// 步驟2:創建消息對象
Message msg = Message.obtain(); // 實例化消息對象
msg.what = 1; // 消息標識
msg.obj = "AA"; // 消息內容存放
// 步驟3:在工作線程中 通過Handler發送消息到消息隊列中
// 多線程可採用AsyncTask、繼承Thread類、實現Runnable
mHandler.sendMessage(msg);
// 步驟4:開啓工作線程(同時啓動了Handler)
// 多線程可採用AsyncTask、繼承Thread類、實現Runnable
- Handler.post
// 步驟1:在主線程中創建Handler實例
private Handler mhandler = new mHandler();
// 步驟2:在工作線程中 發送消息到消息隊列中 & 指定操作UI內容
// 需傳入1個Runnable對象
mHandler.post(new Runnable() {
@Override
public void run() {
... // 需執行的UI操作
}
});
// 步驟3:開啓工作線程(同時啓動了Handler)
// 多線程可採用AsyncTask、繼承Thread類、實現Runnable
2.6 Handler內存泄漏
1、內存泄露的定義:本該被回收的對象不能被回收而停留在堆內存中
2、內存泄露出現的原因:當一個對象已經不再被使用時,本該被回收但卻因爲有另外一個正在使用的對象持有它的引用從而導致它不能被回收,這就導致了內存泄漏
3、主線程的Looper對象的生命週期 = 該應用程序的生命週期
4、在Java中,非靜態內部類 & 匿名內部類都默認持有 外部類的引用
在Handler消息隊列 還有未處理的消息 / 正在處理消息時,此時若需銷燬外部類MainActivity,但由於引用關係,垃圾回收器(GC)無法回收MainActivity,從而造成內存泄漏。
2.6.1 解決方案
- 解決方案1:靜態內部類+弱引用
1、將Handler的子類設置成 靜態內部類,同時,還可加上 使用WeakReference弱引用持有Activity實例。原因:弱引用的對象擁有短暫的生命週期。在垃圾回收器線程掃描時,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。
2、爲了保證Handler中消息隊列中的所有消息都能被執行,此處推薦使用解決方案1解決內存泄露問題,即靜態內部類 + 弱引用的方式。
public class MainActivity extends AppCompatActivity {
public static final String TAG = "carson:";
private Handler showhandler;
// 主線程創建時便自動創建Looper & 對應的MessageQueue
// 之後執行Loop()進入消息循環
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//1. 實例化自定義的Handler類對象
// a. 此處並無指定Looper,故自動綁定當前線程(主線程)的Looper、MessageQueue;
// b. 定義時需傳入持有的Activity實例(弱引用)
showhandler = new FHandler(this);
// 2. 啓動子線程
new Thread() {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// a. 定義要發送的消息
Message msg = Message.obtain();
msg.what = 2;// 消息標識
msg.obj = "BB";// 消息存放
// b. 傳入主線程的Handler & 向其MessageQueue發送消息
showhandler.sendMessage(msg);
}
}.start();
}
// 分析1:自定義Handler子類
// 設置爲:靜態內部類
private static class FHandler extends Handler{
// 定義 弱引用實例
private WeakReference<Activity> reference;
// 在構造方法中傳入需持有的Activity實例
public FHandler(Activity activity) {
// 使用WeakReference弱引用持有Activity實例
reference = new WeakReference<Activity>(activity); }
// 通過複寫handlerMessage() 從而確定更新UI的操作
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 1:
Log.d(TAG, "收到線程1的消息");
break;
case 2:
Log.d(TAG, " 收到線程2的消息");
break;
}
}
}
}
- 解決方案2:當外部類結束生命週期時,清空Handler內消息隊列
當 外部類(此處以Activity爲例)結束生命週期時,清除 Handler消息隊列裏的所有消息。
@Override
protected void onDestroy() {
super.onDestroy();
// 外部類Activity生命週期結束時,同時清空消息隊列 & 結束Handler生命週期
mHandler.removeCallbacksAndMessages(null);
}
3 AsyncTask(Handler+線程池(默認串行))
1、在工作線程中執行任務,如 耗時任務
2、實現工作線程 & 主線程(UI線程)之間的通信,即:將工作線程的執行結果傳遞給主線程,從而在主線程中執行相關的UI操作
3、從而保證線程安全
3.1 優點
- 方便實現異步通信
不需使用 “任務線程(如繼承Thread類)+ Handler”的複雜組合
- 節省資源
採用線程池的緩存線程 + 複用線程,避免了頻繁創建 & 銷燬線程所帶來的系統資源開銷
3.2 缺點
- 實現比較繁瑣,代碼可讀性差
實現一個AsyncTask比較繁瑣,而且往往不用的業務需要實現不同的AsyncTask,導致代碼可讀性差一點,實際上項目用得不多,有其他更好的替代方案。
3.3 實例
public class MainActivity extends AppCompatActivity {
// 線程變量
MyTask mTask;
// 主佈局中的UI組件
Button button,cancel; // 加載、取消按鈕
TextView text; // 更新的UI組件
ProgressBar progressBar; // 進度條
/**
* 步驟1:創建AsyncTask子類
* 注:
* a. 繼承AsyncTask類
* b. 爲3個泛型參數指定類型;若不使用,可用java.lang.Void類型代替
* 此處指定爲:輸入參數 = String類型、執行進度 = Integer類型、執行結果 = String類型
* c. 根據需求,在AsyncTask子類內實現核心方法
*/
private class MyTask extends AsyncTask<String, Integer, String> {
// 方法1:onPreExecute()
// 作用:執行 線程任務前的操作
@Override
protected void onPreExecute() {
text.setText("加載中");
// 執行前顯示提示
}
// 方法2:doInBackground()
// 作用:接收輸入參數、執行任務中的耗時操作、返回 線程任務執行的結果
// 此處通過計算從而模擬“加載進度”的情況
@Override
protected String doInBackground(String... params) {
try {
int count = 0;
int length = 1;
while (count<99) {
count += length;
// 可調用publishProgress()顯示進度, 之後將執行onProgressUpdate()
publishProgress(count);
// 模擬耗時任務
Thread.sleep(50);
}
}catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
// 方法3:onProgressUpdate()
// 作用:在主線程 顯示線程任務執行的進度
@Override
protected void onProgressUpdate(Integer... progresses) {
progressBar.setProgress(progresses[0]);
text.setText("loading..." + progresses[0] + "%");
}
// 方法4:onPostExecute()
// 作用:接收線程任務執行結果、將執行結果顯示到UI組件
@Override
protected void onPostExecute(String result) {
// 執行完畢後,則更新UI
text.setText("加載完畢");
}
// 方法5:onCancelled()
// 作用:將異步任務設置爲:取消狀態
@Override
protected void onCancelled() {
text.setText("已取消");
progressBar.setProgress(0);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 綁定UI組件
setContentView(R.layout.activity_main);
button = (Button) findViewById(R.id.button);
cancel = (Button) findViewById(R.id.cancel);
text = (TextView) findViewById(R.id.text);
progressBar = (ProgressBar) findViewById(R.id.progress_bar);
/**
* 步驟2:創建AsyncTask子類的實例對象(即 任務實例)
* 注:AsyncTask子類的實例必須在UI線程中創建
*/
mTask = new MyTask();
// 加載按鈕按按下時,則啓動AsyncTask
// 任務完成後更新TextView的文本
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
/**
* 步驟3:手動調用execute(Params... params) 從而執行異步線程任務
* 注:
* a. 必須在UI線程中調用
* b. 同一個AsyncTask實例對象只能執行1次,若執行第2次將會拋出異常
* c. 執行任務中,系統會自動調用AsyncTask的一系列方法:onPreExecute() 、doInBackground()、onProgressUpdate() 、onPostExecute()
* d. 不能手動調用上述方法
*/
mTask.execute();
}
});
cancel = (Button) findViewById(R.id.cancel);
cancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 取消一個正在執行的任務,onCancelled方法將會被調用
mTask.cancel(true);
}
});
}
}
4 HandlerThread(Handler+Thread)
HandlerThread內部維護了一個消息隊列,避免多次創建和銷燬子線程來進行操作。
- HandlerThread本質上是一個線程類,它繼承了Thread;
- HandlerThread有自己的內部Looper對象,可以進行looper循環;
- 通過獲取HandlerThread的looper對象傳遞給Handler對象,可以在handleMessage方法中執行異步任務。
- 創建HandlerThread後必須先調用HandlerThread.start()方法,Thread會先調用run方法,創建Looper對象。
4.1 實例
// 步驟1:創建HandlerThread實例對象
// 傳入參數 = 線程名字,作用 = 標記該線程
HandlerThread mHandlerThread = new HandlerThread("handlerThread");
// 步驟2:啓動線程
mHandlerThread.start();
// 步驟3:創建工作線程Handler & 複寫handleMessage()
// 作用:關聯HandlerThread的Looper對象、實現消息處理操作 & 與其他線程進行通信
// 注:消息處理操作(HandlerMessage())的執行線程 = mHandlerThread所創建的工作線程中執行
Handler workHandler = new Handler( handlerThread.getLooper() ) {
@Override
public boolean handleMessage(Message msg) {
...//消息處理。因爲這裏是線程中的Looper,是可以做異步延遲的
try {
//延時操作
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 這裏是不能直接更新UI的,如果想要更新UI必須獲取到主線程的Handler,才能更新
return true;
}
});
// 步驟4:使用工作線程Handler向工作線程的消息隊列發送消息
// 在工作線程中,當消息循環時取出對應消息 & 在工作線程執行相關操作
// a. 定義要發送的消息
Message msg = Message.obtain();
msg.what = 2; //消息的標識
msg.obj = "B"; // 消息的存放
// b. 通過Handler發送消息到其綁定的消息隊列
workHandler.sendMessage(msg);
// 步驟5:結束線程,即停止線程的消息循環
mHandlerThread.quit();
4.2 注意點
- 內存泄漏
關於Handler的內存泄露上面提到,使用HandlerThread要注意這塊
In Android, Handler classes should be static or leaks might occur.
- 連續發送消息
使用HandlerThread時只是開了一個工作線程,當你執行sendMessage n下後,只是將n個消息發送到消息隊列MessageQueue裏排隊,等候派發消息給Handler再進行對應的操作。
5 IntentService
1、線程任務需按順序在後臺執行,比如離線下載
2、不符合多個數據同時請求的場景:所有的任務都在同一個Thread looper裏執行
- IntentService是Service的子類,根據需要處理異步請求(以intent表示)。客戶端通過調用startService(Intent) 發送請求,該Service根據需要啓動,使用工作線程處理依次每個Intent,並在停止工作時停止自身。
- 它擁有較高的優先級,不易被系統殺死(繼承自Service的緣故),因此比較適合執行一些高優先級的異步任務;
- 它內部通過HandlerThread和Handler實現異步操作
- 創建IntentService時,只需實現onHandleIntent和構造方法,onHandleIntent爲異步方法,可以執行耗時操作;
5.1 使用步驟
- 步驟1:定義 IntentService的子類,需複寫onHandleIntent()方法
public class myIntentService extends IntentService {
/**
* 在構造函數中傳入線程名字
**/
public myIntentService() {
// 調用父類的構造函數
// 參數 = 工作線程的名字
super("myIntentService");
}
/**
* 複寫onHandleIntent()方法
* 根據 Intent實現 耗時任務 操作
**/
@Override
protected void onHandleIntent(Intent intent) {
// 根據 Intent的不同,進行不同的事務處理
String taskName = intent.getExtras().getString("taskName");
switch (taskName) {
case "task1":
Log.i("myIntentService", "do task1");
break;
case "task2":
Log.i("myIntentService", "do task2");
break;
default:
break;
}
}
@Override
public void onCreate() {
Log.i("myIntentService", "onCreate");
super.onCreate();
}
/**
* 複寫onStartCommand()方法
* 默認實現 = 將請求的Intent添加到工作隊列裏
**/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i("myIntentService", "onStartCommand");
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
Log.i("myIntentService", "onDestroy");
super.onDestroy();
}
}
- 步驟2:在Manifest.xml中註冊服務
<service android:name=".myIntentService">
<intent-filter >
<action android:name="cn.scu.finch"/>
</intent-filter>
</service>
- 步驟3:在Activity中開啓Service服務
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 同一服務只會開啓1個工作線程
// 在onHandleIntent()函數裏,依次處理傳入的Intent請求
// 將請求通過Bundle對象傳入到Intent,再傳入到服務裏
// 請求1
Intent i = new Intent("cn.scu.finch");
Bundle bundle = new Bundle();
bundle.putString("taskName", "task1");
i.putExtras(bundle);
startService(i);
// 請求2
Intent i2 = new Intent("cn.scu.finch");
Bundle bundle2 = new Bundle();
bundle2.putString("taskName", "task2");
i2.putExtras(bundle2);
startService(i2);
startService(i); //多次啓動
}
}
6 ThreadPool(線程池)
1、重用存在的線程,減少對象創建、消亡的開銷,性能佳
2、可有效控制最大併發線程數,提高系統資源的使用率,同時避免過多資源競爭,
6.1 ThreadPoolExecutor構造參數解釋
Executor threadPool = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAXIMUM_POOL_SIZE,
KEEP_ALIVE,
TimeUnit.SECONDS,
sPoolWorkQueue,
sThreadFactory
);
- corePoolSize: 線程池的核心線程數,默認情況下,核心線程數會一直在線程池中存活,即使它們處理閒置狀態。如果將ThreadPoolExecutor的allowCoreThreadTimeOut屬性設置爲true,那麼閒置的核心線程在等待新任務到來時會執行超時策略,這個時間間隔由keepAliveTime所指定,當等待時間超過keepAliveTime所指定的時長後,核心線程就會被終止。
- maximumPoolSize: 線程池所能容納的最大線程數量,當活動線程數到達這個數值後,後續的新任務將會被阻塞。如果這個無限大永遠不會阻塞,除非開闢的線程超過了CPU承受的最大範圍。
- keepAliveTime: 非核心線程的超時時長,當系統中非核心線程閒置時間超過keepAliveTime之後,則會被回收。如果ThreadPoolExecutor的allowCoreThreadTimeOut屬性設置爲true,則該參數也表示核心線程的超時時長。
- unit: keepAliveTime這個參數的單位,有納秒、微秒、毫秒、秒、分、時、天等。常用的有TimeUnit.MILLISECONDS(毫秒),TimeUnit.SECONDS(秒)以及TimeUnit.MINUTES(分鐘)等。
- workQueue: 線程池中的任務隊列,該隊列主要用來存儲已經被提交但是尚未執行的任務。存儲在這裏的任務是由ThreadPoolExecutor的execute方法提交來的。
- threadFactory: 爲線程池提供創建新線程的功能,這個我們一般使用默認即可。
- handler: 拒絕策略,當線程無法執行新任務時(一般是由於線程池中的線程數量已經達到最大數或者線程池關閉導致的),默認情況下,當線程池無法處理新線程時,會拋出一個RejectedExecutionException。
6.2 實例
// 1. 創建線程池
// 創建時,通過配置線程池的參數,從而實現自己所需的線程池
Executor threadPool = new ThreadPoolExecutor(...);
// 注:在Java中,已內置4種常見線程池,下面會詳細說明
// 2. 向線程池提交任務:execute()
// 說明:傳入 Runnable對象
threadPool.execute(new Runnable() {
@Override
public void run() {
... // 線程執行任務
}
});
// 3. 關閉線程池shutdown()
threadPool.shutdown();
// 關閉線程的原理
// a. 遍歷線程池中的所有工作線程
// b. 逐個調用線程的interrupt()中斷線程(注:無法響應中斷的任務可能永遠無法終止)
// 也可調用shutdownNow()關閉線程:threadPool.shutdownNow()
// 二者區別:
// shutdown:設置 線程池的狀態 爲 SHUTDOWN,然後中斷所有沒有正在執行任務的線程
// shutdownNow:設置 線程池的狀態 爲 STOP,然後嘗試停止所有的正在執行或暫停任務的線程,並返回等待執行任務的列表
// 使用建議:一般調用shutdown()關閉線程池;若任務不一定要執行完,則調用shutdownNow()
6.2 四種線程池
6.2.1 定長線程池(FixedThreadPool)
特點:只有核心線程而且不會被回收、線程數量固定、任務隊列無大小限制(超出的線程任務會在隊列中等待阻塞)
應用場景:控制線程最大併發數
// 1. 創建定長線程池對象 & 設置線程池線程數量固定爲3
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
// 2. 創建好Runnable類線程對象 & 需執行的任務
Runnable task =new Runnable(){
public void run(){
System.out.println("執行任務啦");
}
};
// 3. 向線程池提交任務:execute()
fixedThreadPool.execute(task);
// 4. 關閉線程池
fixedThreadPool.shutdown();
6.2.2 定時線程池(ScheduledThreadPool )
核心線程數量固定、非核心線程數量無限制,不會阻塞(閒置時馬上回收)
應用場景:執行定時以及週期性任務
爲什麼不用Timer做定時和延時任務?
- Timer的特點
1.Timer是單線程模式;
2.如果在執行任務期間某個TimerTask耗時較久,那麼就會影響其它任務的調度;
3.Timer的任務調度是基於絕對時間的,對系統時間敏感;
4.Timer不會捕獲執行TimerTask時所拋出的異常,由於Timer是單線程,所以一旦出現異常,則線程就會終止,其他任務也得不到執行。 - ScheduledThreadPoolExecutor的特點
1.ScheduledThreadPoolExecutor是多線程
2.多線程,單個線程耗時操作不會影響影響其它任務的調度
3.基於相對時間,對系統時間不敏感
4.多線程,單個任務的執行異常不會影響其他線程
// 1. 創建 定時線程池對象 & 設置線程池線程數量固定爲5
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
// 2. 創建好Runnable類線程對象 & 需執行的任務
Runnable task =new Runnable(){
public void run(){
System.out.println("執行任務啦");
}
};
// 3. 向線程池提交任務:schedule()
scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS); // 延遲1s後執行任務
scheduledThreadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);// 延遲10ms後、每隔1000ms執行任務
// 4. 關閉線程池
scheduledThreadPool.shutdown();
6.2.3 可緩存線程池(CachedThreadPool)
特點:只有非核心線程、線程數量不固定(可無限大)、靈活回收空閒線程(具備超時機制,全部回收時幾乎不佔系統資源)、新建線程(無線程可用時),不會阻塞
應用場景:執行大量、耗時少的線程任務
// 1. 創建可緩存線程池對象
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 2. 創建好Runnable類線程對象 & 需執行的任務
Runnable task =new Runnable(){
public void run(){
System.out.println("執行任務啦");
}
};
// 3. 向線程池提交任務:execute()
cachedThreadPool.execute(task);
// 4. 關閉線程池
cachedThreadPool.shutdown();
//當執行第二個任務時第一個任務已經完成
//那麼會複用執行第一個任務的線程,而不用每次新建線程。
6.2.4 單線程化線程池(SingleThreadExecutor)
特點:只有一個核心線程(超過一個任務會阻塞,保證所有任務按照指定順序在一個線程中執行,不需要處理線程同步的問題)
應用場景:不適合併發但可能引起IO阻塞性及影響UI線程響應的操作,如數據庫操作,文件操作等
// 1. 創建單線程化線程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 2. 創建好Runnable類線程對象 & 需執行的任務
Runnable task =new Runnable(){
public void run(){
System.out.println("執行任務啦");
}
};
// 3. 向線程池提交任務:execute()
singleThreadExecutor.execute(task);
// 4. 關閉線程池
singleThreadExecutor.shutdown();
6.3 線程池的注意點
雖然線程池是構建多線程應用程序的強大機制,但使用它並不是沒有風險的。用線程池構建的應用程序容易遭受任何其它多線程應用程序容易遭受的所有併發風險,諸如同步錯誤和死鎖,它還容易遭受特定於線程池的少數其它風險,諸如與池有關的死鎖、資源不足和線程泄漏。
1.死鎖
任何多線程應用程序都有死鎖風險。當一組進程或線程中的每一個都在等待一個只有該組中另一個進程才能引起的事件時,我們就說這組進程或線程 死鎖了。死鎖的最簡單情形是:線程 A 持有對象 X 的獨佔鎖,並且在等待對象 Y 的鎖,而線程 B 持有對象 Y 的獨佔鎖,卻在等待對象 X 的鎖。除非有某種方法來打破對鎖的等待(Java 鎖定不支持這種方法),否則死鎖的線程將永遠等下去。
雖然任何多線程程序中都有死鎖的風險,但線程池卻引入了另一種死鎖可能,在那種情況下,所有池線程都在執行已阻塞的等待隊列中另一任務的執行結果的任務,但這一任務卻因爲沒有未被佔用的線程而不能運行。當線程池被用來實現涉及許多交互對象的模擬,被模擬的對象可以相互發送查詢,這些查詢接下來作爲排隊的任務執行,查詢對象又同步等待着響應時,會發生這種情況。
2.資源不足
線程池的一個優點在於:相對於其它替代調度機制而言,它們通常執行得很好。但只有恰當地調整了線程池大小時纔是這樣的。線程消耗包括內存和其它系統資源在內的大量資源。除了 Thread 對象所需的內存之外,每個線程都需要兩個可能很大的執行調用堆棧。除此以外,JVM 可能會爲每個 Java 線程創建一個本機線程,這些本機線程將消耗額外的系統資源。最後,雖然線程之間切換的調度開銷很小,但如果有很多線程,環境切換也可能嚴重地影響程序的性能。
如果線程池太大,那麼被那些線程消耗的資源可能嚴重地影響系統性能。在線程之間進行切換將會浪費時間,而且使用超出比您實際需要的線程可能會引起資源匱乏問題,因爲池線程正在消耗一些資源,而這些資源可能會被其它任務更有效地利用。除了線程自身所使用的資源以外,服務請求時所做的工作可能需要其它資源,例如 JDBC 連接、套接字或文件。這些也都是有限資源,有太多的併發請求也可能引起失效,例如不能分配 JDBC 連接。
3.併發錯誤
線程池和其它排隊機制依靠使用 wait() 和 notify() 方法,這兩個方法都難於使用。如果編碼不正確,那麼可能丟失通知,導致線程保持空閒狀態,儘管隊列中有工作要處理。使用這些方法時,必須格外小心;即便是專家也可能在它們上面出錯。而最好使用現有的、已經知道能工作的實現,例如 util.concurrent 包。
4.線程泄漏
各種類型的線程池中一個嚴重的風險是線程泄漏,當從池中除去一個線程以執行一項任務,而在任務完成後該線程卻沒有返回池時,會發生這種情況。發生線程泄漏的一種情形出現在任務拋出一個 RuntimeException 或一個 Error 時。如果池類沒有捕捉到它們,那麼線程只會退出而線程池的大小將會永久減少一個。當這種情況發生的次數足夠多時,線程池最終就爲空,而且系統將停止,因爲沒有可用的線程來處理任務。
有些任務可能會永遠等待某些資源或來自用戶的輸入,而這些資源又不能保證變得可用,用戶可能也已經回家了,諸如此類的任務會永久停止,而這些停止的任務也會引起和線程泄漏同樣的問題。如果某個線程被這樣一個任務永久地消耗着,那麼它實際上就被從池除去了。對於這樣的任務,應該要麼只給予它們自己的線程,要麼只讓它們等待有限的時間。
5.請求過載
僅僅是請求就壓垮了服務器,這種情況是可能的。在這種情形下,我們可能不想將每個到來的請求都排隊到我們的工作隊列,因爲排在隊列中等待執行的任務可能會消耗太多的系統資源並引起資源缺乏。在這種情形下決定如何做取決於您自己;在某些情況下,您可以簡單地拋棄請求,依靠更高級別的協議稍後重試請求,您也可以用一個指出服務器暫時很忙的響應來拒絕請求。
7 結尾
有句話說的不錯,好記憶不如爛筆頭。Android學習過程中,最好的方式就是記錄,記多了看多了不知不覺就成專家了。而寫博客就是幫自己梳理知識點的最好的方式,你可以嘗試去多讀幾篇類似的文章,然後自己梳理記下來,寫出自己的博客,你會受益匪淺而且極易深刻,不信你試試!!共勉吧~