java線程池原理詳解

導論

在開發中,我們會遇到需要多個線程執行的任務。如果我們每次都通過new Thread來創建線程執行任務的話,在線程很多的情況下,是會非常銷燬資源,影響程序運行的。Java提供了線程池Executor來幫助我們處理需要用到多個線程的情況,線程池可以用來存儲多個線程,通過創建線程池,我們可以有以下幾個好處:

  • 重用線程池的線程,避免了因爲重複創建、銷燬線程而到來的性能開銷。
  • 能夠控制線程池中的最大併發數,避免了線程間因爲互相搶佔系統資源而導致的阻塞現象。
  • 能夠對線程進行簡單的管理,並且提供了定時執行、循環執行線程的功能。

下面我將詳細的介紹關於線程池的知識點

TreadPoolExecutor的參數說明

ThreadPoolExecutor是線程池的真實實現,它的構造方法中有一系列參數來設置線程池。

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
  • corePoolSize
    指的是最大核心線程的數量。默認情況下,核心線程即使是在很長一段時間處於閒置狀態,也不會銷燬。但是當線程池設置了allowCoreThreadTimeOut參數爲true時,且核心線程閒置的時間超過了線程池中的keepAliveTime變量所指定的時長後,也會被終止。

  • maximumPoolSize
    指的是最大線程數量。當線程池中的核心線程數達到最大後,且工作隊列也滿了後,線程池就會立即創建新線程來執行任務,而maximumPoolSize則指定了線程池中的最大線程數,當線程數達到了最大線程數,並且工作隊列中的任務也滿了,這時在添加任務時,會根據設置處理方式,對線程池進行處理。

  • keepAliveTime
    指的是非核心線程的超時時間,當非核心線程閒置時間超過了這個之後,線程將會被回收。同時,當設置了allowCoreTreadTimeOut時,keepAliveTime也會作用核心線程。

  • unit
    keepAliveTime的單位,這是一個枚舉值,常用的有

    TimeUnit.DAYS;               //天
    TimeUnit.HOURS;             //小時
    TimeUnit.MINUTES;           //分鐘
    TimeUnit.SECONDS;           //秒
    TimeUnit.MILLISECONDS;      //毫秒
    TimeUnit.MICROSECONDS;      //微妙
    TimeUnit.NANOSECONDS;       //納秒
    
  • workQueue
    任務隊列,當我們調用線程池中的execute方法提交的Runnable對象就會存儲在這裏,在被線程池中的線程給執行。常用的有

    • ArrayBlockingQueue 是一個有界緩存等待隊列,可以指定緩存隊列的大小,當正在執行的線程數等於corePoolSize時,多餘的元素緩存在ArrayBlockingQueue隊列中等待有空閒的線程時繼續執行,當ArrayBlockingQueue已滿時,加入ArrayBlockingQueue失敗,會開啓新的線程去執行,當線程數已經達到最大的maximumPoolSizes時,再有新的元素嘗試加入ArrayBlockingQueue時,如果沒有設置handler時會報錯。

    • LinkedBlockingQueue 是一個無界緩存等待隊列。當前執行的線程數量達到corePoolSize的數量時,剩餘的元素會在阻塞隊列裏等待。(所以在使用此阻塞隊列時maximumPoolSizes就相當於無效了),每個線程完全獨立於其他線程。生產者和消費者使用獨立的鎖來控制數據的同步,即在高併發的情況下可以並行操作隊列中的數據。

    • SynchronousQueue SynchronousQueue沒有容量,是無緩衝等待隊列,是一個不存儲元素的阻塞隊列,會直接將任務交給消費者,必須等隊列中的添加元素被消費後才能繼續添加新的元素。擁有公平(FIFO)和非公平(LIFO)策略,非公平側羅會導致一些數據永遠無法被消費的情況?使用SynchronousQueue阻塞隊列一般要求maximumPoolSizes爲無界(Integer.MAX_VALUE),避免線程拒絕執行操作。

  • ThreadFactory
    用於自定義創建線程的方式,是一個接口,裏面只有newThread方法。

  • handler
    用來設置拒絕處理任務時的策略,這時候可能是任務隊列滿了,或者是無法成功的執行任務,常用的有

    • ThreadPoolExecutor.AbortPolicy 丟棄任務並拋出RejectedExecutionException異常。 默認值
    • ThreadPoolExecutor.DiscardPolicy 也是丟棄任務,但是不拋出異常。
    • ThreadPoolExecutor.DiscardOldestPolic 丟棄隊列最前面的任務,然後重新嘗試執行任務(重複此過程)
    • ThreadPoolExecutor.CallerRunsPolicy 任務被拒絕添加後,會調用當前線程池的所在的線程去執行被拒絕的任務。

TreadPoolExecutor的源碼分析

  1. 線程池的狀態分析
     //原子狀態控制數
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    //29比特位
    private static final int COUNT_BITS = Integer.SIZE - 3;
    //實際容量 2^29-1
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

    // runState is stored in the high-order bits
    // runState存儲在高位中
    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;

    // Packing and unpacking ctl 打包和解壓ctl

    // 解壓runState
    private static int runStateOf(int c)     { return c & ~CAPACITY; }
    // 解壓workerCount
    private static int workerCountOf(int c)  { return c & CAPACITY; }
    // 打包ctl
    private static int ctlOf(int rs, int wc) { return rs | wc; }

線程池中採用AtomicInteger的clt來將workCount(工作線程數)和runState(運行狀態)壓縮在一起。其中用3個比特位來記錄runState,用29比特位來記錄workCount。所以從上面我們可以知道,實際上可以允許的最大線程數爲2^29-1。
接着我們來看下每個狀態的含義

狀態 含義
RUNNING 運行態,線程池可以接收任務,並處理任務
SHUTDOWN 關閉態,不接收新任務,但會處理完隊列中的任務,調用了shutdown()方法會處於該狀態
STOP 停止態,不接受新任務,並且不會處理隊列中的任務,打斷正在執行的任務,調用stop()方法會處於該狀態
TIYDING 整理態,所有任務已經結束,workerCount = 0 ,將執行terminated()方法
TERMINATED 結束態,terminated() 方法已完成
  1. 任務的執行
 private final BlockingQueue<Runnable> workQueue;              //任務緩存隊列,用來存放等待執行的任務
private final ReentrantLock mainLock = new ReentrantLock();   //線程池的主要狀態鎖,對線程池狀態(比如線程池大小
                                                              //、runState等)的改變都要使用這個鎖
private final HashSet<Worker> workers = new HashSet<Worker>();  //用來存放工作集
 
private volatile long  keepAliveTime;    //線程存貨時間   
private volatile boolean allowCoreThreadTimeOut;   //是否允許爲核心線程設置存活時間
private volatile int   corePoolSize;     //核心池的大小(即線程池中的線程數目大於這個參數時,提交的任務會被放進任務緩存隊列)
private volatile int   maximumPoolSize;   //線程池最大能容忍的線程數
 
private volatile int   poolSize;       //線程池中當前的線程數
 
private volatile RejectedExecutionHandler handler; //任務拒絕策略
 
private volatile ThreadFactory threadFactory;   //線程工廠,用來創建線程
 
private int largestPoolSize;   //用來記錄線程池中曾經出現過的最大線程數
 
private long completedTaskCount;   //用來記錄已經執行完畢的任務個數

以上是一些比較重要的參數具體含義註釋中有介紹,就不多說了。
接着我們來看下ThreadLocalPool中最核心的方法execute(),這個方法是用於我們向線程池提交任務的:

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        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);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

這裏的ctl包含了runState和workCount的值,首先當線程池中的線程數小於corePoolSize時,就會通過addWorker來開啓個新線程,並利用新線程執行任務。同時addWorker會檢查運行狀態和線程數,如果添加失敗的化(可能是調用了shutdown或stop),會更新一下clt,防止在addWorker的過程中clt發生了改變。

當我們線程數大於核心線程數,或者沒有成功添加任務時,我們判斷線程池的運行狀態和工作隊列是否能夠添加任務(如果滿了則不行),如果滿足條件,我們仍然需要更新一下ctl的值,因爲可能在執行方法的過程中可能會有線程的銷燬、添加,或者調用了shutdown和stop方法。之後如果不是running狀態(即調用了shutdown()和stop()方法),我們將拒絕添加任務。當線程池處於RUNNING狀態,並且線程個數爲0時,也會調用addWorker創建線程。

上面的代碼核心時addWorker方法,我們來看看他是怎樣實現創建線程

private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }

        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            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;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

代碼有點長,但整體並不難理解,首先是一個死循環,在循環中會根據傳過來的參數判斷是否能夠創建一個新的worker,這裏的worker是一個實現了Runnable的類,創建線程就是在它裏面完成的,我們可以來看下它的構造函數:

//Worker
/** Thread this worker is running in.  Null if factory fails. */
        final Thread thread;
        /** Initial task to run.  Possibly null. */
        Runnable firstTask;
        /** Per-thread task counter */
        volatile long completedTasks;

        /**
         * Creates with given first task and thread from ThreadFactory.
         * @param firstTask the first task (null if none)
         */
        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }

看到了嗎,在Worker中包含着線程,以及任務和完成任務數,並且在構造方式中,就會利用getThreadFactory()創建線程。可以說Worker就是線程池中執行線程的對象。既然Worker實現了Runnable那麼,我們就來看下它其中的run方法:

//Worker
      public void run() {
          runWorker(this);
      }
      final void runWorker(Worker w) {
      Thread wt = Thread.currentThread();
      Runnable task = w.firstTask;
      w.firstTask = null;
      w.unlock(); // allow interrupts
      boolean completedAbruptly = true;
      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;
      } finally {
          processWorkerExit(w, completedAbruptly);
      }
  }

可以看出runWorker方法首先會執行傳入的任務,然後再不停的調用getTask來獲取任務,這個方法是在ThreadPoolExecutor中的,那麼任務從哪裏獲取呢?當然是你傳入的任務緩存隊列啦。我們來看看getTask的方法

private Runnable getTask() {
        boolean timedOut = false; // 判斷是否閒置實間超時了

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

            // 在這裏當線程池的狀態不爲SHUTDOWN和RUNNING或者狀態爲SHUTDOWN並且任務隊列爲空時,返回null,代表了無法
            //在接收任務了。
            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;
            }
        }
    }

上面關鍵的地方我都加了註釋,應該不會太難理解,就是不斷的從任務隊列中取出任務返回給worker處理。

好了我們返回addWorker方法中

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

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }

在循環中,當線程池處於stop狀態或者特殊情況下工作隊列爲空時,返回false,即代表無法添加worker,並且在循環的內部還有一個循環,噹噹前的核心線程數大於規定的最大核心線程數或則,總線程數達到了最大值,則返回false,無法添加。否則的話,將會使stl中的線程數加一,並且跳出循環。

		//addWorker
		boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            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;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;

之後嘗試創建worker,並且如果worker中利用threadFactory創建的線程不爲null的話,就會直接用這個線程執行任務。如何threadFactory創建線程失敗的話,同樣會導致返回false。

總結

通過上面的源碼分析,我們可以得到如下的結論:

  • 當線程池中的線程數小於最大核心線程數時,每獲得一個任務,就會創建一個新的Thread去執行這個任務。
  • 當線程數>=corePoolSize,每獲得一個任務時,會首先嚐試把任務放進緩存隊列中去,但是當存入緩存隊列失敗時(一般時隊列滿了),則會創建新的線程來執行這個任務。
  • 如果當前線程池中的線程數目達到maximumPoolSize,並且任務隊列滿了,則會採取任務拒絕策略進行處理;
  • 如果線程池中的線程數量大於 corePoolSize時,如果某線程空閒時間超過keepAliveTime,線程將被終止,直至線程池中的線程數目不大於corePoolSize;如果允許爲核心池中的線程設置存活時間,那麼核心池中的線程空閒時間超過keepAliveTime,線程也會被終止。

常用的線程池

一般情況下我們使用線程池不會直接調用它的構造方法來配置參數,而是調用線程池中的特定方法來獲得特定的線程池。常用的有以下四種

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
public static ScheduledExecutorService newScheduledThreadPool(
            int corePoolSize, ThreadFactory threadFactory) {
        return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
    }
public ScheduledThreadPoolExecutor(int corePoolSize,
                                       ThreadFactory threadFactory) {
        super(corePoolSize, Integer.MAX_VALUE,
              DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
              new DelayedWorkQueue(), threadFactory);
    }
  • FixedThreadPool
    FixedThreadPool只會有數量固定的核心線程,也就意味着當所有線程都處於活動狀態時,新任務來時都會處於等待狀態,並且這些線程沒有超時機制,不會被回收,除非關閉線程池。可以看出構造時傳入了LinkedBlockingQueue,所以任務緩存隊列也是沒有大小限制的。

  • SingeleThreadPool
    這個線程池中只有一個核心類,它確保了所有任務都在一個線程中按順序執行。它的好處就是這些任務之間不需要處理併發的問題。

  • CachedThreadPool
    這個線程池中只有非核心線程,且最大線程數位Integer.MAX_VALUE,這就意味着它可以擁有大量的
    線程。這裏的緩存隊列是SynchronousQueue,這個隊列很特殊,它不具有存儲功能,由於使用較少,這裏就不多說了。總的來說這種線程池適合於大量耗時較少的任務的情形。

  • ScheduledThreadPool
    這個線程的核心線程數是固定的,而非核心線程數是沒有限制的,並且當被限制時會被立即回收。它能夠實現延遲任務以及週期性的任務。

參考 https://www.cnblogs.com/dolphin0520/p/3932921.html
《Android開發藝術探索》

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