從原理上粗略認知Android線程池(ThreadPoolExecutor)

在開發過程中,可能經常遇到下面的開發場景,即一個列表加載網絡圖片,比如加載100個網絡圖片,我們是不是就要去新建100個線程來下載對應的圖片,這種做法當然是能夠實現功能的,但是100個圖片就100個線程也太誇張了,可能導致線程之間互相搶佔系統資源以及線程創建和銷燬會給應用帶來額外的性能開銷。所以這個時候我們就想到了利用線程池來實現這個功能,在Android開發藝術探索一書中,總結了線程池的以下三個優點。

1、重用線程池中的線程,避免因爲線程池的創建和銷燬所帶來的性能開銷;

2、有效控制線程的最大併發數,避免大量線程之間因爲槍戰系統資源而出現的阻塞現象;

3、對線程進行簡單的管理,並提供定時執行以及制定間隔循環執行等功能;

這裏強調一點第三點,在Android中的線程池管理的線程不是開發者提供給線程池的線程,而是管理線程池自己創建的線程,這一點我之前沒弄明白,這裏強調一下。實際上,Android中的使用線程池也只需要開發者提供給線程Runnbble或者Callable對象實例。在調用線程池的execute方法時提供Runnable對象實例,而在調用submit方法時可以提供Callable對象實例,由於submit方法實際上還是調用了execute方法,所以本篇文章就以execute方法使用線程池來進行代碼分析。

1、實例化線程池

在分析線程池原理之前來先看看實例化線程池的一些參數含義,這個參數的介紹很多博客文章都有,這裏也簡單說下。先看看ThreadPoolExecutor構造方法

 public ThreadPoolExecutor(int corePoolSize, int maxPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler rejectedHandler)

可以看到這裏有7個參數,之前一篇文章說的很形象,七個參數就是星期一到星期天,那麼這七個參數的含義是什麼呢?

corePoolSize

核心線程數,默認情況下,核心線程會一直在線程池中存活,即便它們處於閒置狀態。但是如果將ThreadPoolExecutor的allowCoreThreadTimeOut設置爲true,那麼閒置的核心線程也會在超時時被終止,這個超時的時間間隔由keepAliveTime參數來決定。

maxPoolSize

最大線程數,上面說到了corePoolSize表示核心線程數,那麼肯定就有非核心線程數,所以非核心線程數就是最大線程數減去核心線程數。

keepAliveTime

這個是非核心線程或者核心線程(allowCoreThreadTimeOut爲true時)在閒置的時候能夠存留的時間。

unit

前一個參數keekAliveTime指明瞭存留時間數值,但是沒有說明計時單位,而這個參數就表示時間單位,可以是毫秒(TimeUnit.MILLISECONDS)、秒(TimeUnit.SECONDS)以及分鐘(TimeUnit.MINUTES)等。

workQueue

BlockingQueue類型的隊列對象,當Runnable對象超過核心線程時,剩下的Runnbable對象就放入workQueue中,當workQueue中放滿了,之後繼續添加Runnable對象時,則線程池會新建非核心線程來執行Runnable對象。需要說明的是BlockingQueue是阻塞隊列,當它沒有數據了,繼續去取時會阻塞取數據的操作所在線程,當添加新的數據時會激活阻塞線程,這一點對於線程池能夠正常工作十分重要。常用的BlockingQueue類型隊列有ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue以及SynchronousQueue,不同的BlockingQueue會影響存儲進去的數據(Runnable對象)取出來時的順序。

threadFactory

線程工廠,顧名思義其提供創建新線程的功能。ThreadFactory是一個接口,它只有一個方法

Thread newThread(Runnable var1);

我們如果想自定義線程池創建線程的方法即可通過重寫ThreadFactory來實現,不過一般都是使用默認的方法來創建線程,所以這裏就不多介紹了。

rejectedHandler

當線程無法執行新任務時,則通過該參數來提供回調。RejectedExecutionHandler也是一個接口,也只有一個方法

void rejectedExecution(Runnable var1, ThreadPoolExecutor var2);

如果開發者想處理無法執行新任務的情況,可以通過重寫這個參數來實現,這個很簡單,不影響本篇博客從原理上分析線程池,所以這裏也使用默認值。

2、線程池使用方法

這裏介紹一個及其簡單的使用線程池的例子

int coreThreadNum = 3;
int maxThreadNum = 5;
int keepAliveTime = 1;
Executor poolExecutor = new ThreadPoolExecutor(coreThreadNum,maxThreadNum,keepAliveTime, TimeUnit.SECONDS,new LinkedBlockingQueue<>(128));

for (int i = 0; i < 30; i++) {
    final int finalI = i;
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            SystemClock.sleep(2000);
            Log.d(TAG, "run: " + finalI);
        }
    };
    poolExecutor.execute(runnable);
}

這裏核心線程數爲3,非核心線程數爲2,閒置留存時間爲1秒,BlockQueue的容量爲128,輸出時爲每間隔2秒輸出三組數據,最終完成各個Runnable任務的執行(圖片和代碼來自Android開發之線程池使用總結 這篇博客)
這裏寫圖片描述

3、線程池執行任務規則說明

按照Android開發藝術探索藝術中所說,ThreadPoolExecutor執行任務時大致遵循如下規則

1、如果線程池中線程數量未達到核心線程數量是,那麼直接新建並啓動一個核心線程執行任務;

2、如果線程池中的線程數量已經達到或者超過核心線程數量,那麼任務會被添加到workQueue中等待執行;

3、如果步驟2中無法將任務添加到workQueue中,這通常是workQueue已經滿了,這個時候如果線程數量未達到規定的最大值,那麼會立刻啓動一個非核心線程來執行任務;

4、如果步驟3中的線程數量已經達到了線程池規定的最大值,那麼就拒絕執行此任務,並通過調用RejectedExecutionHandler的rejectedExecution方法來通知調用者;

這裏需要說明的是無論是第1步中的說的核心線程還是第3步中說的非核心線程,實際上只是這麼稱呼而已,都是普通的線程,沒有什麼特殊的,當線程中的線程數量小於核心線程池時新創建的線程被稱爲核心線程,大於核心線程數時創建的線程就叫做非核心線程,但是核心線程和非核心線程創建之後便無差別,都開始從workQueue中領任務執行。在覈心線程和非核心線程閒置時,我們前面說過keepAliveTime時間到時會銷燬非核心線程,實際上線程池在操作時只是從總的線程(核心線程和非核心線程)中隨機的銷燬線程,使得總的線程數量達到核心線程數而已。關於這一點會在後面的代碼分析中加以論證。

這一節指出了線程池執行任務時的規則,那麼爲什麼是這個規則呢,這裏我們就來看看源碼。因爲線程池添加任務時執行如下代碼

poolExecutor.execute(runnable);

那麼我們就來看看ThreadPoolExecutor的execute方法

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();

    int c = ctl.get();
    //workerCountOf方法獲取線程池中有幾個線程了
    if (workerCountOf(c) < corePoolSize) {

        //addWorker方法用來添加新的線程,true參數表示添加的是核心線程
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }

    //workQueue.offer方法添加Runnable任務和workQueue隊列中
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    //添加不了就通過addWorker方法添加非核心線程
    else if (!addWorker(command, false))
        //非核心線程添加失敗就通知調用者
        reject(command);
}

從對execute的源碼和註釋可以看出以上線程池執行基本規則是對的

4、workQueue中的任務如何被執行的

我們知道線程池中最多創建maxThreadSize這麼多個線程,很可能很多任務都在workQueue中等待被線程執行,那麼這些任務都怎麼被放到線程中去執行的呢?

我們把目光放到execute方法中說的添加新線程的addWorker方法上來,這裏截取addWorker的部分關鍵代碼

try {
    //在Worker的構造函數中新建線程
    w = new Worker(firstTask);
    final Thread t = w.thread;
    if (t != null) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            // Recheck while holding lock.
            // Back out on ThreadFactory failure or if
            // shut down before lock acquired.
            int rs = runStateOf(ctl.get());

            if (rs < SHUTDOWN ||
                (rs == SHUTDOWN && firstTask == null)) {
                if (t.isAlive()) // precheck that t is startable
                    throw new IllegalThreadStateException();
                workers.add(w);
                int s = workers.size();
                if (s > largestPoolSize)
                    largestPoolSize = s;
                workerAdded = true;
            }
        } finally {
            mainLock.unlock();
        }
        if (workerAdded) {
            //啓動線程
            t.start();
            workerStarted = true;
        }
    }
}

由註釋可知,新線程時通過Workder的構造函數創建的

Worker(Runnable firstTask) {
    setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);
}

這裏可以看出新建線程時,傳了參數this,也就是Worker對象本身(Worker是Runnable的實現),同時也可以看出我們自己提供ThreadFactory的話也會在這裏起作用。

在啓動線程的時候,會調用現成的run方法

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

而這裏的target就是上面的this,也就是Worker,所以新建線程的target.run方法自然就調用到了Worker的run方法中,繼而調用到了ThreadPoolExecutor的runWorker方法,這裏看看其部分代碼

try {
    while (task != null || (task = getTask()) != null) {
        w.lock();
        // If pool is stopping, ensure thread is interrupted;
        // if not, ensure thread is not interrupted.  This
        // requires a recheck in second case to deal with
        // shutdownNow race while clearing interrupt
        if ((runStateAtLeast(ctl.get(), STOP) ||
             (Thread.interrupted() &&
              runStateAtLeast(ctl.get(), STOP))) &&
            !wt.isInterrupted())
            wt.interrupt();
        try {
            beforeExecute(wt, task);
            Throwable thrown = null;
            try {
                task.run();
            } catch (RuntimeException x) {
                thrown = x; throw x;
            } catch (Error x) {
                thrown = x; throw x;
            } catch (Throwable x) {
                thrown = x; throw new Error(x);
            } finally {
                afterExecute(task, thrown);
            }
        } finally {
            task = null;
            w.completedTasks++;
            w.unlock();
        }
    }
    completedAbruptly = false;
} 

runWorker方法中有一個while循環,其不斷的通過getTask方法從workQueue中獲取Runnable任務並通過task.run來執行任務(這裏的task是Runnable對象)。這裏的代碼可以看出各個線程(核心和非核心)不斷的從workerQueue中獲取任務來執行,執行任務時並不區分核心和非核心線程。

我們再進一步的看看getTask方法

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);

        // Are workers subject to culling?
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

正常情況下getTask方法返回workQueue中的Runnable任務,我們注意到如下代碼

boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

這個代碼決定了workQueue中無任務時,會有多少線程不被銷燬,因爲當workQueue中無任務時,執行workQueue.take方法會阻塞線程,而執行workQueue.poll時並不會阻塞線程。被阻塞掉的線程會保留,而沒有阻塞的線程就會被銷燬。這裏很明顯阻塞和銷燬的線程都是隨機的,allowCoreThreadTimeOut 爲false的情況下會阻塞掉corePoolSize數量的線程,當allowCoreThreadTimeOut 爲true時,所有線程都會被銷燬。

public E take() throws InterruptedException {
    E x;
    int c = -1;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly();
    try {
        while (count.get() == 0) {
            //線程阻塞
            notEmpty.await();
        }
        x = dequeue();
        c = count.getAndDecrement();
        if (c > 1)
            notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
    if (c == capacity)
        signalNotFull();
    return x;
}

根據代碼take方法代碼可知,在workQueue中無任務時,take方法所處的線程被阻塞,這就是說此時調用getTask的runWorker所在線程將被阻塞掉,而我們再看ThreadPoolExecutor中調用的workQueue.offer(command)壓入Runnable任務時,會調用workQueue的offer方法

public boolean offer(E e) {
    if (e == null) throw new NullPointerException();
    final AtomicInteger count = this.count;
    if (count.get() == capacity)
        return false;
    int c = -1;
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    putLock.lock();
    try {
        if (count.get() < capacity) {
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();//激活線程
        }
    } finally {
        putLock.unlock();
    }
    if (c == 0)
        signalNotEmpty();
    return c >= 0;
}

看註釋位置signal將激活一個線程,同時因爲workQueue中有了任務數據,所以getTask不再阻塞,線程繼續運行獲取到新的任務來執行。

到這裏應該就說清楚了workQueue中添加的任務時如何被執行的,以及任務執行完後,以及後面新添加任務時,線程根據參數的變化(銷燬、阻塞、激活)。

而且通過本小節,應該清楚實際上核心線程和非核心線程沒啥區別,需要的收常見n(核心) + m(非核心)個線程,大家一起從workQueue中取活來幹,活幹完了,就從n + m個線程中隨機幹掉m個線程,以確保下次有活幹的時候,還是有n個線程能幹活。

需要注意到是本小節實際上await和signal的關係沒有講清楚,實際上我現在也不清楚,關於Condition的await和signal使用,以後弄清出了再另寫一篇博文吧,這裏只需要知道起到阻塞和激活線程的作用就行了。

5、ThreadPoolExecutor的submit方法

ThreadPoolExecutor的submit方法有以下幾個重載方法

    /**
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     */
    public Future<?> submit(Runnable task) 

    /**
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     */
    public <T> Future<T> submit(Runnable task, T result) 

    /**
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     */
    public <T> Future<T> submit(Callable<T> task) 

由於執行原理基本相同,這裏選取最後一個重載方法來說明submit的執行原理,這裏直接引用Java多線程-線程池ThreadPoolExecutor的submit返回值Future 這篇文章的例子

try{
    ExecutorService executor = Executors.newFixedThreadPool(2);
    //創建一個Callable,3秒後返回String類型
    Callable myCallable = new Callable() {
        @Override
        public String call() throws Exception {
            Thread.sleep(3000);
            System.out.println("calld方法執行了");
            return "call方法返回值";
        }
    };
    System.out.println("提交任務之前 "+ getStringDate());
    Future future = executor.submit(myCallable);
    System.out.println("提交任務之後,獲取結果之前 "+getStringDate());
    System.out.println("獲取返回值: "+ future.get());
    System.out.println("獲取到結果之後 "+getStringDate());
}catch (InterruptedException e){
    e.printStackTrace();
}catch (ExecutionException e){
    e.printStackTrace();
}

輸出內容爲

提交任務之前 12:13:01
提交任務之後,獲取結果之前 12:13:01
calld方法執行了
獲取返回值: call方法返回值
獲取到結果之後 12:13:04

從例子可以看到,submit方法和execute方法不同

1、submit方法提供了Future 類型的返回值;
2、Future的get方法返回submit參數Callable 對象重載重載方法call的返回值;
3、Future對象的get方法是一個阻塞方法,會阻塞掉當前所在線程,知道獲取到返回值(call方法執行完畢);

這裏就從源碼的角度簡單分析下,首先看看submit方法

public <T> Future<T> submit(Callable<T> task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task);
    execute(ftask);
    return ftask;
}

這裏通過newTaskFor方法實例化一個Future對象,看看newTaskFor方法,直接通過new FutureTask,來創建一個FureTask對象(Future的子類對象,同時也是Runnable的子類對象)。

public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;       // ensure visibility of callable
}

我們知道線程執行時會執行到Runnable的run方法,這裏看看FureTask的run方法,這裏只取其run方法的部分代碼

try {
    Callable<V> c = callable;
    if (c != null && state == NEW) {
        V result;
        boolean ran;
        try {
            result = c.call();
            ran = true;
        } catch (Throwable ex) {
            result = null;
            ran = false;
            setException(ex);
        }
        if (ran)
            set(result);
    }
}

在FutureTask的run方法中,我們看到執行了Callable的call方法,也就是我們在例子中重載的call方法,到此ThreadPoolExecutor的submit方法源碼分析就算完成。那麼我們提到的Future的get方法是一個阻塞方法,怎麼理解呢,實際上submit方法返回的是一個FutureTask對象,那麼看看FutureTask對象的get方法便知

public V get() throws InterruptedException, ExecutionException {
    int s = state;
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    return report(s);
}

很明顯,這裏的awaitDone方法便是一個阻塞方法阻塞了當前線程直到call方法執行完成,而爲什麼get方法返回的值是call返回值呢,這個需要結合FutureTask的run、get以及report這幾個方法來分析,這實際上不難,感興趣的童鞋可以自己分析看看。

6、常用的線程池總結

在開發過程中,我們可以自己通過不同核心線程、總線程等參數來構造線程池,也可以通過Executors提供的方法來直接構造常用的線程池,這裏直接引用Android開發之線程池使用總結 這篇文章的例子吧。常用的線程池有FixedThreadPool、SingleThreadExecutor、CachedThreadPool以及ScheduledThreadPool幾種,它們有不同的使用環境,這裏來看看

6.1、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核心線程數和最大線程數一樣,說明在FixedThreadPool中沒有非核心線程,所有的線程都是核心線程,且線程的超時時間爲0,說明核心線程即使在沒有任務可執行的時候也不會被銷燬(這樣可讓FixedThreadPool更快速的響應請求),最後的線程隊列是一個LinkedBlockingQueue,但是LinkedBlockingQueue卻沒有參數,這說明線程隊列的大小爲Integer.MAX_VALUE(2的31次方減1)。看完參數,我們大概也就知道了FixedThreadPool的工作特點了,當所有的核心線程都在執行任務的時候,新任務將會被放入workQueue隊列中,等待被線程執行。這裏來看一個例子

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);  
}  

執行結果如下:
這裏寫圖片描述

從執行結果可以看出FixedThreadPool線程池,創建了上線程,且三個線程從workQueue中取任務來執行,由於每個任務的執行時間一樣,所以從結果上來看,就是每隔3秒鐘三個線程就差不多同時輸出一次日誌。這裏30個任務唄執行完成後,FixedThreadPool創建的是哪個線程不會被銷燬,當workQueue中有新的任務時會被激活來繼續執行任務,這樣就避免了線程創建和銷燬的系統開銷。

6.2、SingleThreadExecutor

SingleThreadExecutor和FixedThreadPool很像,不同的就是SingleThreadExecutor的核心線程數只有1個,最大線程也是1個,如下所示

public static ExecutorService newSingleThreadExecutor() {  
    return new FinalizableDelegatedExecutorService  
        (new ThreadPoolExecutor(1, 1,  
                                0L, TimeUnit.MILLISECONDS,  
                                new LinkedBlockingQueue<Runnable>()));  
}  

從源碼我們可以看出,實際上SingleThreadExecutor就是FixedThreadPool的一個特例(至於爲啥名字不叫SingleThreadExecutor,這個其實也無所謂反正都是返回ThreadPoolExecutor就是了)。當FixedThreadPool的參數爲1時,實際上就是創建了一個SingleThreadExecutor。使用SingleThreadExecutor,線程池中始終只有一個線程執行任務,這實際上就相當與我們開了一個單線程執行任務,不過這裏的這種單線程會不斷的workQueue中獲取任務來執行,而且在任務執行完成之後,該線程不會被銷燬。這裏來看個例子

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);  
}  

執行效果如下:
這裏寫圖片描述
從執行效果也可以驗證我們前面說的一個線程從workQueue中獲取任務執行的說法,這裏每隔1秒總輸出一條日誌。

6.3、CachedThreadPool

CachedTreadPool一個特點是它可以根據程序的運行情況自動來調整線程池中的線程數量,CachedThreadPool源碼如下

public static ExecutorService newCachedThreadPool() {  
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,  
                                  60L, TimeUnit.SECONDS,  
                                  new SynchronousQueue<Runnable>());  
}  

通過源碼可知,CachedThreadPool中沒有和線程,而最大線程數是Integer.MAX_VALUE,實際上可以理解爲CachedThreadPool可以創建無窮多個非核心線程。同時,CachedThreadPool的超時時間爲60秒,這說明創建的這些非核心線程在沒有任務執行時,60秒回會被銷燬掉。而當有新的任務時如果有空閒的線程則由空閒線程執行任務,如果沒有空閒的線程則,創建新的線程來執行任務。由此可知CacheThreadPool適合有大量任務請求時使用,比如文章開始提到的列表的大量圖片下載請求。這裏來看看一個使用CachedThreadPool的例子

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);  
}  

由於每次添加完任務之後都停兩秒再添加新任務,並且添加的任務不費時,可以推測這裏所有的任務都會由同一個線程來執行(因爲每次添加新任務的時候都有空閒的線程),運行結果如下

這裏寫圖片描述

由輸出日誌可以看到和我們的推測一致,基本是隔兩秒由同一線程輸出日誌,這裏把代碼稍微改動一下

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);  
}  

每個任務在執行的過程中都先休眠兩秒,但每隔一秒便向CachedTreadPool中添加一個線程,也就是說一個線程都沒有執行完任務的時候,就會有新的任務添加到workQueue中,那麼按照上面說的,CachedTreadPool便會創建新的線程來執行任務。再進一步分析一下線程1在執行第一個任務時,添加了第二個任務,此時線程1不空閒,那麼線程池會添加新的線程2執行第二個任務,當第三個任務到來之時,線程1應該已經空閒了(也不一定),那麼第三個任務可能就是線程1來執行,當有第四個任務時線程2應該空閒了,第四個任務便由線程2來執行,所以理論上只需要兩個線程便能完成任務執行,而實際上由於CPU的時間片輪轉並不是公平的,所以可能兩個線程不夠用,但可以肯定的是基本3個線程可以搞定這裏的任務執行,所以就有了下面的運行結果

這裏寫圖片描述

6.4、ScheduledThreadPool

ScheduledThreadPool是一個具有定時定期執行任務功能的線程池,源碼如下:

public ScheduledThreadPoolExecutor(int corePoolSize) {  
    super(corePoolSize, Integer.MAX_VALUE,  
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,  
          new DelayedWorkQueue());  
}  

從源碼可以看出,ScheduledThreadPool的核心線程數量是固定的,但是非核心線程是無窮大。這裏的DEFAULT_KEEPALIVE_MILLIS參數值如下

private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;

但是單位卻是MILLISECONDS,所以非核心線程閒置時,將會被很快回收。

使用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); 

這裏將延遲一秒鐘啓動任務,ScheduledThreadPool線程池將正常的執行任務。

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秒執行,從字面上理解這個延遲執行任務和上面的延遲定時執行任務好像最終表現都一樣,這裏具體怎麼回事這篇博文也不深究了,這裏只需要知道有一個ScheduledThreadPool線程池可以控制任務的執行時間就行了,在實際開發過程中有對應使用場景的時候再來研究具體使怎麼回事
,當然感興趣的童鞋也可以去研究研究ScheduledThreadPoolExecutor這個類。

7、總結

到這裏關於Android線程池的介紹基本就告一段落了,通過以上介紹,我們應該能夠明白文章開頭提出的線程池的以下幾個優點了

1、重用線程池中的線程,避免因爲線程池的創建和銷燬所帶來的性能開銷;
2、有效控制線程的最大併發數,避免大量線程之間因爲槍戰系統資源而出現的阻塞現象;
3、對線程進行簡單的管理,並提供定時執行以及制定間隔循環執行等功能;

另外通過本篇博文的介紹還應該明白線程池的基本工作原理,在這裏非常感謝上面提到文獻的幾位同學的貢獻,本文直接引用了以上幾篇文獻的代碼和一些博文內容,非常感謝,如有侵權,麻煩告知刪除,謝謝!

參考文獻

1、深入理解在Android中線程池的使用
2、Android開發之線程池使用總結
3、Android開發藝術探索 [任玉剛]
4、併發編程3:線程池的使用與執行流程
5、Java進階(三)多線程開發關鍵技術
6、Java多線程-線程池ThreadPoolExecutor的submit返回值Future

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