1. 程式人生 > >Android開發之執行緒池使用總結

Android開發之執行緒池使用總結


執行緒池算是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,如下:

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
--------------------- 
作者:_江南一點雨 
來源:CSDN 
原文:https://blog.csdn.net/u012702547/article/details/52259529 
版權宣告:本文為博主原創文章,轉載請附上博文連結!