Android中的Thread與AsyncTask的區別

原文鏈接
http://www.zhihu.com/question/30804052/answer/49562693
這篇也可以取看看
http://blog.csdn.net/u012403246/article/details/45949963

Android 原生的 AsyncTask.java 是對線程池的一個封裝,使用其自定義的 Executor 來調度線程的執行方式(併發還是串行),並使用 Handler 來完成子線程和主線程數據的共享。

預先了解 AsyncTask,必先對線程池有所瞭解。

一般情況下,如果使用子線程去執行一些任務,那麼使用 new Thread 的方式會很方便的創建一個線程,如果涉及到主線程和子線程的通信,我們將使用 Handler(一般需要刷新 UI 的適合用到)。

如果我們創建大量的(特別是在短時間內,持續的創建生命週期較長的線程)野生線程,往往會出現如下兩方面的問題:

每個線程的創建與銷燬(特別是創建)的資源開銷是非常大的;
大量的子線程會分享主線程的系統資源,從而會使主線程因資源受限而導致應用性能降低。

各位開發一線的前輩們爲了解決這個問題,引入了線程池(ThreadPool)的概念,也就是把這些野生的線程圈養起來,統一的管理他們。線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。

那麼線程池是如何使用的呢?

我們可以通過ThreadPoolExecutor來創建一個線程池。

new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue, handler);

創建一個線程池需要輸入幾個參數:
corePoolSize(線程池的基本大小):當提交一個任務到線程池時,線程池會創建一個線程來執行任務,即使其他空閒的基本線程能夠執行新任務也會創建線程,等到需要執行的任務數大於線程池基本大小時就不再創建。如果調用了線程池的 prestartAllCoreThreads 方法,線程池會提前創建並啓動所有基本線程。
runnableTaskQueue(任務隊列):用於保存等待執行的任務的阻塞隊列。 可以選擇以下幾個阻塞隊列。
ArrayBlockingQueue:是一個基於數組結構的有界阻塞隊列,此隊列按 FIFO(先進先出)原則對元素進行排序。
LinkedBlockingQueue:一個基於鏈表結構的阻塞隊列,此隊列按 FIFO (先進先出) 排序元素,吞吐量通常要高於 ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool() 使用了這個隊列。
SynchronousQueue:一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQueue,靜態工廠方法 Executors.newCachedThreadPool 使用了這個隊列。
PriorityBlockingQueue:一個具有優先級的無限阻塞隊列。
maximumPoolSize(線程池最大大小):線程池允許創建的最大線程數。如果隊列滿了,並且已創建的線程數小於最大線程數,則線程池會再創建新的線程執行任務。值得注意的是如果使用了無界的任務隊列這個參數就沒什麼效果。
ThreadFactory:用於設置創建線程的工廠,可以通過線程工廠給每個創建出來的線程設置更有意義的名字。
RejectedExecutionHandler(飽和策略):當隊列和線程池都滿了,說明線程池處於飽和狀態,那麼必須採取一種策略處理提交的新任務。這個策略默認情況下是 AbortPolicy,表示無法處理新任務時拋出異常。以下是JDK1.5提供的四種策略。
AbortPolicy:直接拋出異常。
CallerRunsPolicy:只用調用者所在線程來運行任務。
DiscardOldestPolicy:丟棄隊列裏最近的一個任務,並執行當前任務。
DiscardPolicy:不處理,丟棄掉。
當然也可以根據應用場景需要來實現 RejectedExecutionHandler 接口自定義策略。如記錄日誌或持久化不能處理的任務。
keepAliveTime(線程活動保持時間):線程池的工作線程空閒後,保持存活的時間。所以如果任務很多,並且每個任務執行的時間比較短,可以調大這個時間,提高線程的利用率。
TimeUnit(線程活動保持時間的單位):可選的單位有天(DAYS),小時(HOURS),分鐘(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
如何向線程池提交線程任務呢?

我們可以使用線程池的 execute 提交的任務,但是 execute 方法沒有返回值,所以無法判斷任務是否被線程池執行成功:
threadsPool.execute(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
}
});

  1. 我們也可以使用 submit 方法來提交任務,它會返回一個 future,那麼我們可以通過這個 future 來判斷任務是否執行成功,通過 future 的 get 方法來獲取返回值,get 方法會阻塞住直到任務完成,而使用 get(long timeout, TimeUnit unit) 方法則會阻塞一段時間後立即返回,這時有可能任務沒有執行完:

Future future = executor.submit(harReturnValuetask);
try {
Object s = future.get();
} catch (InterruptedException e) {
// 處理中斷異常
} catch (ExecutionException e) {
// 處理無法執行任務異常
} finally {
// 關閉線程池
executor.shutdown();
}

線程池是如何關閉的呢?

ThreadPoolExecutor 提供了兩個方法,用於線程池的關閉,分別是 shutdown()和shutdownNow(),其中:
shutdown():不會立即終止線程池,而是要等所有任務緩存隊列中的任務都執行完後才終止,但再也不會接受新的任務;
shutdownNow():立即終止線程池,並嘗試打斷正在執行的任務,並且清空任務緩存隊列,返回尚未執行的任務。

線程池的原理?

線程池中比較重要的規則:

corePoolSize 與 maximumPoolSize
由於 ThreadPoolExecutor 將根據 corePoolSize 和 maximumPoolSize 設置的邊界自動調整池大小,當新任務在方法 execute(java.lang.Runnable) 中提交時:

如果運行的線程少於 corePoolSize,則創建新線程來處理請求,即使其他輔助線程是空閒的;
如果設置的 corePoolSize 和 maximumPoolSize 相同,則創建的線程池是大小固定的,如果運行的線程數與 corePoolSize 相同,當有新請求過來時,若 workQueue 未滿,則將請求放入 workQueue 中,等待有空閒的線程去從 workQueue 中取任務並處理
如果運行的線程多於 corePoolSize 而少於 maximumPoolSize,則僅當隊列滿時才創建新線程去處理請求;
如果運行的線程多於 corePoolSize 並且等於 maximumPoolSize,若隊列已經滿了,則通過RejectedExecutionHandler 所指定的策略來處理新請求;
如果將 maximumPoolSize 設置爲基本的無界值(如 Integer.MAX_VALUE),則允許池適應任意數量的併發任務
也就是說,處理任務的優先級爲:

corePoolSize > workQueue > maximumPoolSize,如果三者都滿了,使用 RejectedExecutionHandler 處理被拒絕的任務。
當池中的線程數大於 corePoolSize 的時候,多餘的線程會等待 keepAliveTime 長的時間,如果無請求可處理就自行銷燬。

workQueue線程池所使用的緩衝隊列,該緩衝隊列的長度決定了能夠緩衝的最大數量,緩衝隊列有三種通用策略:

直接提交。工作隊列的默認選項是 SynchronousQueue,它將任務直接提交給線程而不保持它們。在此,如果不存在可用於立即運行任務的線程,則試圖把任務加入隊列將失敗,因此會構造一個新的線程。此策略可以避免在處理可能具有內部依賴性的請求集時出現鎖。直接提交通常要求無界 maximumPoolSizes 以避免拒絕新提交的任務。當命令以超過隊列所能處理的平均數連續到達時,此策略允許無界線程具有增長的可能性;
無界隊列。使用無界隊列(例如,不具有預定義容量的 LinkedBlockingQueue)將導致在所有 corePoolSize 線程都忙時新任務在隊列中等待。這樣,創建的線程就不會超過 corePoolSize(因此,maximumPoolSize 的值也就無效了)。當每個任務完全獨立於其他任務,即任務執行互不影響時,適合於使用無界隊列;例如,在 Web 頁服務器中。這種排隊可用於處理瞬態突發請求,當命令以超過隊列所能處理的平均數連續到達時,此策略允許無界線程具有增長的可能性;
有界隊列。當使用有限的 maximumPoolSizes 時,有界隊列(如 ArrayBlockingQueue)有助於防止資源耗盡,但是可能較難調整和控制。隊列大小和最大池大小可能需要相互折衷:使用大型隊列和小型池可以最大限度地降低 CPU 使用率、操作系統資源和上下文切換開銷,但是可能導致人工降低吞吐量。如果任務頻繁阻塞(例如,如果它們是 I/O 邊界),則系統可能爲超過您許可的更多線程安排時間。使用小型隊列通常要求較大的池大小,CPU 使用率較高,但是可能遇到不可接受的調度開銷,這樣也會降低吞吐量。

ThreadFactory
使用 ThreadFactory 創建新線程。如果沒有另外說明,則在同一個 ThreadGroup 中一律使用 Executors.defaultThreadFactory() 創建線程,並且這些線程具有相同的 NORM_PRIORITY 優先級和非守護進程狀態。通過提供不同的 ThreadFactory,可以改變線程的名稱、線程組、優先級、守護進程狀態等等。如果執行 newThread 時 ThreadFactory 未能創建線程(返回 null),則執行程序將繼續運行,但不能執行任何任務。

接下來我們看一下 ThreadPoolExecutor 中最重要的 execute 方法:

public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
//如果線程數小於基本線程數,則創建線程並執行當前任務
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
//如線程數大於等於基本線程數或線程創建失敗,則將當前任務放到工作隊列中。
if (runState == RUNNING && workQueue.offer(command)) {
if (runState != RUNNING || poolSize == 0)
ensureQueuedTaskHandled(command);
}
//如果線程池不處於運行中或任務無法放入隊列,並且當前線程數量小於最大允許的線程數量,
則創建一個線程執行任務。
else if (!addIfUnderMaximumPoolSize(command))
//拋出RejectedExecutionException異常
reject(command); // is shutdown or saturated
}
}

線程池容量的動態調整?

ThreadPoolExecutor 提供了動態調整線程池容量大小的方法:setCorePoolSize() 和setMaximumPoolSize():
setCorePoolSize:設置核心池大小
setMaximumPoolSize:設置線程池最大能創建的線程數目大小
當上述參數從小變大時,ThreadPoolExecutor 進行線程賦值,還可能立即創建新的線程來執行任務。

線程池的監控?

通過線程池提供的參數進行監控。線程池裏有一些屬性在監控線程池的時候可以使用
taskCount:線程池需要執行的任務數量。
completedTaskCount:線程池在運行過程中已完成的任務數量。小於或等於 taskCount。
largestPoolSize:線程池曾經創建過的最大線程數量。通過這個數據可以知道線程池是否滿過。如等於線程池的最大大小,則表示線程池曾經滿了。
getPoolSize: 線程池的線程數量。如果線程池不銷燬的話,池裏的線程不會自動銷燬,所以這個大小隻增不減。
getActiveCount:獲取活動的線程數。
通過擴展線程池進行監控。通過繼承線程池並重寫線程池的 beforeExecute,afterExecute 和 terminated 方法,我們可以在任務執行前,執行後和線程池關閉前幹一些事情。如監控任務的平均執行時間,最大執行時間和最小執行時間等。

使用線程池的風險?

雖然線程池是構建多線程應用程序的強大機制,但使用它並不是沒有風險的。用線程池構建的應用程序容易遭受任何其它多線程應用程序容易遭受的所有併發風險,諸如同步錯誤和死鎖,它還容易遭受特定於線程池的少數其它風險,諸如與池有關的死鎖、資源不足和線程泄漏。

死鎖
任何多線程應用程序都有死鎖風險。當一組進程或線程中的每一個都在等待一個只有該組中另一個進程才能引起的事件時,我們就說這組進程或線程 死鎖了。死鎖的最簡單情形是:線程 A 持有對象 X 的獨佔鎖,並且在等待對象 Y 的鎖,而線程 B 持有對象 Y 的獨佔鎖,卻在等待對象 X 的鎖。除非有某種方法來打破對鎖的等待(Java 鎖定不支持這種方法),否則死鎖的線程將永遠等下去。

雖然任何多線程程序中都有死鎖的風險,但線程池卻引入了另一種死鎖可能,在那種情況下,所有池線程都在執行已阻塞的等待隊列中另一任務的執行結果的任務,但這一任務卻因爲沒有未被佔用的線程而不能運行。當線程池被用來實現涉及許多交互對象的模擬,被模擬的對象可以相互發送查詢,這些查詢接下來作爲排隊的任務執行,查詢對象又同步等待着響應時,會發生這種情況。

資源不足
線程池的一個優點在於:相對於其它替代調度機制(有些我們已經討論過)而言,它們通常執行得很好。但只有恰當地調整了線程池大小時纔是這樣的。線程消耗包括內存和其它系統資源在內的大量資源。除了 Thread 對象所需的內存之外,每個線程都需要兩個可能很大的執行調用堆棧。除此以外,JVM 可能會爲每個 Java 線程創建一個本機線程,這些本機線程將消耗額外的系統資源。最後,雖然線程之間切換的調度開銷很小,但如果有很多線程,環境切換也可能嚴重地影響程序的性能。

如果線程池太大,那麼被那些線程消耗的資源可能嚴重地影響系統性能。在線程之間進行切換將會浪費時間,而且使用超出比您實際需要的線程可能會引起資源匱乏問題,因爲池線程正在消耗一些資源,而這些資源可能會被其它任務更有效地利用。除了線程自身所使用的資源以外,服務請求時所做的工作可能需要其它資源,例如 JDBC 連接、套接字或文件。這些也都是有限資源,有太多的併發請求也可能引起失效,例如不能分配 JDBC 連接。

併發錯誤
線程池和其它排隊機制依靠使用 wait() 和 notify() 方法,這兩個方法都難於使用。如果編碼不正確,那麼可能丟失通知,導致線程保持空閒狀態,儘管隊列中有工作要處理。使用這些方法時,必須格外小心;即便是專家也可能在它們上面出錯。而最好使用現有的、已經知道能工作的實現,例如 util.concurrent 包。

線程泄漏
各種類型的線程池中一個嚴重的風險是線程泄漏,當從池中除去一個線程以執行一項任務,而在任務完成後該線程卻沒有返回池時,會發生這種情況。發生線程泄漏的一種情形出現在任務拋出一個 RuntimeException 或一個 Error 時。如果池類沒有捕捉到它們,那麼線程只會退出而線程池的大小將會永久減少一個。當這種情況發生的次數足夠多時,線程池最終就爲空,而且系統將停止,因爲沒有可用的線程來處理任務。

有些任務可能會永遠等待某些資源或來自用戶的輸入,而這些資源又不能保證變得可用,用戶可能也已經回家了,諸如此類的任務會永久停止,而這些停止的任務也會引起和線程泄漏同樣的問題。如果某個線程被這樣一個任務永久地消耗着,那麼它實際上就被從池除去了。對於這樣的任務,應該要麼只給予它們自己的線程,要麼只讓它們等待有限的時間。

請求過載
僅僅是請求就壓垮了服務器,這種情況是可能的。在這種情形下,我們可能不想將每個到來的請求都排隊到我們的工作隊列,因爲排在隊列中等待執行的任務可能會消耗太多的系統資源並引起資源缺乏。在這種情形下決定如何做取決於您自己;在某些情況下,您可以簡單地拋棄請求,依靠更高級別的協議稍後重試請求,您也可以用一個指出服務器暫時很忙的響應來拒絕請求。

參考資料:

Java 7之多線程線程池

聊聊併發(三)——JAVA線程池的分析和使用

Java 理論與實踐: 線程池與工作隊列

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章