線程池算是Android開發中非常常用的一個東西了,隻要涉及到線程的地方,大多數情況下都會涉及到線程池。Android開發中線程池的使用和Java中線程池的使用基本一緻。那麼今天我想來總結一下Android開發中線程池的使用。
OK,假如說我想做一個新聞應用,ListView上有一個item,每個item上都有一張圖檔需要從網絡上加載,如果不使用線程池,你可能通過下面的方式來開啟一個新線程:
new Thread(new Runnable() {
@Override
public void run() {
//網絡通路
}
}).start();
這種用法主要存在以下3點問題:
1.使用new Thread()建立線程存在的問題
1.針對每一個item都建立一個新線程,這樣會導緻頻繁的建立線程,線程執行完之後又被回收,又會導緻頻繁的GC
2.這麼多線程缺乏統一管理,各線程之間互相競争,降低程式的運作效率,手機頁面卡頓,甚至會導緻程式崩潰
3.如果一個item滑出頁面,則要停止該item上圖檔的加載,但是如果使用這種方式來建立線程,則無法實作線程停止執行
如果使用線程池,我們就可以很好的解決以上三個問題。
2.使用線程池的好處
1.重用已經建立的好的線程,避免頻繁建立進而導緻的頻繁GC
2.控制線程并發數,合理使用系統資源,提高應用性能
3.可以有效的控制線程的執行,比如定時執行,取消執行等
OK,我們知道Android中的線程池其實源于Java,Java中和線程有關的東東叫做Executor,Executor本身是一個接口,這個接口有一個非常有用的實作類叫做ThreadPoolExecutor,如下:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIyVGduV2QvwVe0lmdhJ3ZvwFM38CXlZHbvN3cpR2Lc1TPB10QGtWUCpEMJ9CXsxWam9CXwADNvwVZ6l2c052bm9CXUJDT1wkNhVzLcRnbvZ2LcZXUYpVd1kmYr50MZV3YyI2cKJDT29GRjBjUIF2LcRHelR3LcJzLctmch1mclRXY39jN2kTMwEzMwEDMygDM2EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
Android中常用的線程池都是通過對ThreadPoolExecutor進行不同配置來實作的,那麼我們今天就從這這個ThreadPoolExecutor來開始吧!
3.ThreadPoolExecutor
ThreadPoolExecutor有四個重載的構造方法,我們這裡來說說參數最多的那一個重載的構造方法,這樣大家就知道其他方法參數的含義了,如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
這裡是7個參數(我們在開發中用的更多的是5個參數的構造方法),OK,那我們來看看這裡七個參數的含義:
corePoolSize 線程池中核心線程的數量
maximumPoolSize 線程池中最大線程數量
keepAliveTime 非核心線程的逾時時長,當系統中非核心線程閑置時間超過keepAliveTime之後,則會被回收。如果ThreadPoolExecutor的allowCoreThreadTimeOut屬性設定為true,則該參數也表示核心線程的逾時時長
unit 第三個參數的機關,有納秒、微秒、毫秒、秒、分、時、天等
workQueue 線程池中的任務隊列,該隊列主要用來存儲已經被送出但是尚未執行的任務。存儲在這裡的任務是由ThreadPoolExecutor的execute方法送出來的。
threadFactory 為線程池提供建立新線程的功能,這個我們一般使用預設即可
handler 拒絕政策,當線程無法執行新任務時(一般是由于線程池中的線程數量已經達到最大數或者線程池關閉導緻的),預設情況下,當線程池無法處理新線程時,會抛出一個RejectedExecutionException。
針對于workQueue參數我多說幾點:workQueue是一個BlockingQueue類型,那麼這個BlockingQueue又是什麼呢?它是一個特殊的隊列,當我們從BlockingQueue中取資料時,如果BlockingQueue是空的,則取資料的操作會進入到阻塞狀态,當BlockingQueue中有了新資料時,這個取資料的操作又會被重新喚醒。同理,如果BlockingQueue中的資料已經滿了,往BlockingQueue中存資料的操作又會進入阻塞狀态,直到BlockingQueue中又有新的空間,存資料的操作又會被沖洗喚醒。BlockingQueue有多種不同的實作類,下面我舉幾個例子來說一下:
1.ArrayBlockingQueue:這個表示一個規定了大小的BlockingQueue,ArrayBlockingQueue的構造函數接受一個int類型的資料,該資料表示BlockingQueue的大小,存儲在ArrayBlockingQueue中的元素按照FIFO(先進先出)的方式來進行存取。
2.LinkedBlockingQueue:這個表示一個大小不确定的BlockingQueue,在LinkedBlockingQueue的構造方法中可以傳一個int類型的資料,這樣建立出來的LinkedBlockingQueue是有大小的,也可以不傳,不傳的話,LinkedBlockingQueue的大小就為Integer.MAX_VALUE,源碼如下:
/**
* Creates a {@code LinkedBlockingQueue} with a capacity of
* {@link Integer#MAX_VALUE}.
*/
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
/**
* Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity.
*
* @param capacity the capacity of this queue
* @throws IllegalArgumentException if {@code capacity} is not greater
* than zero
*/
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
3.PriorityBlockingQueue:這個隊列和LinkedBlockingQueue類似,不同的是PriorityBlockingQueue中的元素不是按照FIFO來排序的,而是按照元素的Comparator來決定存取順序的(這個功能也反映了存入PriorityBlockingQueue中的資料必須實作了Comparator接口)。
4.SynchronousQueue:這個是同步Queue,屬于線程安全的BlockingQueue的一種,在SynchronousQueue中,生産者線程的插入操作必須要等待消費者線程的移除操作,Synchronous内部沒有資料緩存空間,是以我們無法對SynchronousQueue進行讀取或者周遊其中的資料,元素隻有在你試圖取走的時候才有可能存在。我們可以了解為生産者和消費者互相等待,等到對方之後然後再一起離開。
OK,這是ThreadPoolExecutor的構造方法參數的解釋,我們的線程送出到線程池之後又是按照什麼樣的規則去運作呢?OK,它們遵循如下規則:
1.execute一個線程之後,如果線程池中的線程數未達到核心線程數,則會立馬啟用一個核心線程去執行
2.execute一個線程之後,如果線程池中的線程數已經達到核心線程數,且workQueue未滿,則将新線程放入workQueue中等待執行
3.execute一個線程之後,如果線程池中的線程數已經達到核心線程數但未超過非核心線程數,且workQueue已滿,則開啟一個非核心線程來執行任務
4.execute一個線程之後,如果線程池中的線程數已經超過非核心線程數,則拒絕執行該任務
OK,基于以上講解,我們來看一個Demo:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
poolExecutor = new ThreadPoolExecutor(3, 5,
1, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(128));
}
public void btnClick(View view) {
for (int i = 0; i < 30; i++) {
final int finalI = i;
Runnable runnable = new Runnable() {
@Override
public void run() {
SystemClock.sleep(2000);
Log.d("google_lenve_fb", "run: " + finalI);
}
};
poolExecutor.execute(runnable);
}
}
執行結果如下:
OK,由于核心線程數為3,workQueue的大小為128,是以我們的線程的執行應該是先啟動三個核心線程來執行任務,剩餘的27個任務全部存在workQueue中,等待核心線程空餘出來之後執行。OK,那我把構造ThreadPoolExecutor的參數修改一下,來驗證一下我們上面的結論正确與否:
poolExecutor = new ThreadPoolExecutor(3, 30,
1, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(6));
如上,我把最大線程數改為30,而把線程隊列大小改為6(實際開發中 不會這樣來設定,這裡隻是為了驗證結論),我們來看看執行結果:
OK,首先列印出來0,1,2說明往核心線程添加了三個任務,然後将3,4,5,6,7,8六個任務添加到了任務隊列中,接下來要添加的任務因為核心線程已滿,隊列已滿是以就直接開一個非核心線程來執行,是以後添加的任務反而會先執行(3,4,5,6,7,8都在隊列中等着),是以我們看到的列印結果是先是0~2,然後9~29,然後3~8,當然,我們在實際開發中不會這樣來配置最大線程數和線程隊列。那如果我們需要自己來配置這些參數,該如何配置呢?參考一下AsyncTask,AsyncTask部分源碼如下:
public abstract class AsyncTask<Params, Progress, Result> {
private static final String LOG_TAG = "AsyncTask";
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final int KEEP_ALIVE = 1;
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
}
};
private static final BlockingQueue<Runnable> sPoolWorkQueue =
new LinkedBlockingQueue<Runnable>(128);
/**
* An {@link Executor} that can be used to execute tasks in parallel.
*/
public static final Executor THREAD_POOL_EXECUTOR
= new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
....
....
}
我們看到,核心線程數為手機CPU數量+1(cpu數量擷取方式Runtime.getRuntime().availableProcessors()),最大線程數為手機CPU數量×2+1,線程隊列的大小為128,OK,那麼小夥伴們在以後使用線程池的過程中可以參考這個再結合自己的實際情況來配置參數。
OK,那麼和線程池有關的最基本的ThreadPoolExecutor我們就說完了,接下來我們就來看看系統配置好的提供給我們的線程池。
4.FixedThreadPool
FixedThreadPool是一個核心線程數量固定的線程池,建立方式如下:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
源碼如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
我們看到核心線程數和最大線程數一樣,說明在FixedThreadPool中沒有非核心線程,所有的線程都是核心線程,且線程的逾時時間為0,說明核心線程即使在沒有任務可執行的時候也不會被銷毀(這樣可讓FixedThreadPool更快速的響應請求),最後的線程隊列是一個LinkedBlockingQueue,但是LinkedBlockingQueue卻沒有參數,這說明線程隊列的大小為Integer.MAX_VALUE(2的31次方減1),OK,看完參數,我們大概也就知道了FixedThreadPool的工作特點了,當所有的核心線程都在執行任務的時候,新的任務隻能進入線程隊列中進行等待,直到有線程被空閑出來。OK,我們來看一個Demo:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 30; i++) {
final int finalI = i;
Runnable runnable = new Runnable(){
@Override
public void run() {
SystemClock.sleep(3000);
Log.d("google_lenve_fb", "run: "+ finalI);
}
};
fixedThreadPool.execute(runnable);
}
執行結果如下:
這執行結果也和我們想的一緻,先往核心線程中添加三個任務,剩餘任務進入到workQueue中等待,當有空閑的核心線程時就執行任務隊列中的任務。
5.SingleThreadExecutor
singleThreadExecutor和FixedThreadPool很像,不同的就是SingleThreadExecutor的核心線程數隻有1,如下:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
使用SingleThreadExecutor的一個最大好處就是可以避免我們去處理線程同步問題,其實如果我們把FixedThreadPool的參數傳個1,效果不就和SingleThreadExecutor一緻了,來看個Demo:
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 30; i++) {
final int finalI = i;
Runnable runnable = new Runnable() {
@Override
public void run() {
Log.d("google_lenve_fb", "run: " + Thread.currentThread().getName() + "-----" + finalI);
SystemClock.sleep(1000);
}
};
singleThreadExecutor.execute(runnable);
}
執行效果如下:
6.CachedThreadPool
CachedTreadPool一個最大的優勢是它可以根據程式的運作情況自動來調整線程池中的線程數量,CachedThreadPool源碼如下:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
我們看到,CachedThreadPool中是沒有核心線程的,但是它的最大線程數卻為Integer.MAX_VALUE,另外,它是有線程逾時機制的,逾時時間為60秒,這裡它使用了SynchronousQueue作為線程隊列,SynchronousQueue的特點上文已經說過了,這裡不再贅述。那麼我們送出到CachedThreadPool消息隊列中的任務在執行的過程中有什麼特點呢?由于最大線程數為無限大,是以每當我們添加一個新任務進來的時候,如果線程池中有空閑的線程,則由該空閑的線程執行新任務,如果沒有空閑線程,則建立新線程來執行任務。根據CachedThreadPool的特點,我們可以在有大量任務請求的時候使用CachedThreadPool,因為當CachedThreadPool中沒有新任務的時候,它裡邊所有的線程都會因為逾時而被終止。OK,我們來看一個Demo:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 30; i++) {
final int finalI = i;
Runnable runnable = new Runnable(){
@Override
public void run() {
Log.d("google_lenve_fb", "run: " + Thread.currentThread().getName() + "----" + finalI);
}
};
cachedThreadPool.execute(runnable);
SystemClock.sleep(2000);
}
每次添加完任務之後我都停兩秒在添加新任務,由于這裡的任務執行不費時,我們可以猜測這裡所有的任務都使用同一個線程來執行(因為每次添加新任務的時候都有空閑的線程),運作結果如下:
和我們的想法基本一緻。OK,那如果我把代碼稍微改一下:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 30; i++) {
final int finalI = i;
Runnable runnable = new Runnable(){
@Override
public void run() {
SystemClock.sleep(2000);
Log.d("google_lenve_fb", "run: " + Thread.currentThread().getName() + "----" + finalI);
}
};
cachedThreadPool.execute(runnable);
SystemClock.sleep(1000);
}
每個任務在執行的過程中都先休眠兩秒,但是我向線程池添加任務則是每隔1s添加一個任務,這樣的話,添加第一個任務時先開新線程,添加第二個任務時,由于第一個新線程尚未執行完,是以又開一個新線程,添加第三個任務時,第一個線程已經空閑下來了,直接第一個線程來執行第三個任務,依此類推。我們來看看運作結果:
7.ScheduledThreadPool
ScheduledThreadPool是一個具有定時定期執行任務功能的線程池,源碼如下:
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
我們可以看到,它的核心線程數量是固定的(我們在構造的時候傳入的),但是非核心線程是無窮大,當非核心線程閑置時,則會被立即回收。
使用ScheduledThreadPool時,我們可以通過如下幾個方法來添加任務:
1.延遲啟動任務:
public ScheduledFuture<?> schedule(Runnable command,
long delay, TimeUnit unit);
示例代碼:
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
Runnable runnable = new Runnable(){
@Override
public void run() {
Log.d("google_lenve_fb", "run: ----");
}
};
scheduledExecutorService.schedule(runnable, 1, TimeUnit.SECONDS);
2.延遲定時執行任務:
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
延遲initialDelay秒後每個period秒執行一次任務。示例代碼:
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
Runnable runnable = new Runnable(){
@Override
public void run() {
Log.d("google_lenve_fb", "run: ----");
}
};
scheduledExecutorService.scheduleAtFixedRate(runnable, 1, 1, TimeUnit.SECONDS);
延遲1秒之後每隔1秒執行一次新任務。
3.延遲執行任務
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
第一次延遲initialDelay秒,以後每次延遲delay秒執行一個任務。示例代碼:
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
Runnable runnable = new Runnable(){
@Override
public void run() {
Log.d("google_lenve_fb", "run: ----");
}
};
scheduledExecutorService.scheduleWithFixedDelay(runnable, 1, 1, TimeUnit.SECONDS);
第一次延遲1秒之後,以後每次也延遲1秒執行。
OK,至此,Android開發中常用的線程池就說完了。
8.線程池其他常用功能
1.shutDown() 關閉線程池,不影響已經送出的任務
2.shutDownNow() 關閉線程池,并嘗試去終止正在執行的線程
3.allowCoreThreadTimeOut(boolean value) 允許核心線程閑置逾時時被回收
4.submit 一般情況下我們使用execute來送出任務,但是有時候可能也會用到submit,使用submit的好處是submit有傳回值,舉個栗子:
public void submit(View view) {
List<Future<String>> futures = new ArrayList<>();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 5, 1,
TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>());
for (int i = 0; i < 10; i++) {
Future<String> taskFuture = threadPoolExecutor.submit(new MyTask(i));
//将每一個任務的執行結果儲存起來
futures.add(taskFuture);
}
try {
//周遊所有任務的執行結果
for (Future<String> future : futures) {
Log.d("google_lenve_fb", "submit: " + future.get());
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
class MyTask implements Callable<String> {
private int taskId;
public MyTask(int taskId) {
this.taskId = taskId;
}
@Override
public String call() throws Exception {
SystemClock.sleep(1000);
//傳回每一個任務的執行結果
return "call()方法被調用----" + Thread.currentThread().getName() + "-------" + taskId;
}
}
使用submit時我們可以通過實作Callable接口來實作異步任務。在call方法中執行異步任務,傳回值即為該任務的傳回值。Future是傳回結果,傳回它的isDone屬性表示異步任務執行成功!
5. 自定義線程池
除了使用submit來定義線程池擷取線程執行結果之外,我們也可以通過自定義ThreadPoolExecutor來實作這個功能,如下:
public void customThreadPool(View view) {
final MyThreadPool myThreadPool = new MyThreadPool(3, 5, 1, TimeUnit.MINUTES, new LinkedBlockingDeque<Runnable>());
for (int i = 0; i < 10; i++) {
final int finalI = i;
Runnable runnable = new Runnable(){
@Override
public void run() {
SystemClock.sleep(100);
Log.d("google_lenve_fb", "run: " + finalI);
}
};
myThreadPool.execute(runnable);
}
}
class MyThreadPool extends ThreadPoolExecutor{
public MyThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
Log.d("google_lenve_fb", "beforeExecute: 開始執行任務!");
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
Log.d("google_lenve_fb", "beforeExecute: 任務執行結束!");
}
@Override
protected void terminated() {
super.terminated();
//當調用shutDown()或者shutDownNow()時會觸發該方法
Log.d("google_lenve_fb", "terminated: 線程池關閉!");
}
}
執行結果如下:
D/google_lenve_fb: beforeExecute: 開始執行任務!
D/google_lenve_fb: run: 0
D/google_lenve_fb: beforeExecute: 任務執行結束!
D/google_lenve_fb: beforeExecute: 開始執行任務!
D/google_lenve_fb: run: 1
D/google_lenve_fb: beforeExecute: 任務執行結束!
D/google_lenve_fb: beforeExecute: 開始執行任務!
D/google_lenve_fb: run: 2
D/google_lenve_fb: beforeExecute: 任務執行結束!
OK,以上就是關于線程池的使用總結。。
參考資料
1.http://blog.csdn.net/u010687392/article/details/49850803
2.《Android開發藝術探索》
以上。
本文涉及到的Demo下載下傳http://download.csdn.net/detail/u012702547/9608586