Java線程池初步理解

前言:在慕課網上學習劍指Java面試-Offer直通車時所做的筆記.

目錄

第一章 基本概念

1.1 Fork/Join框架

1.2 爲什麼要使用線程池

第二章 源碼理解

2.1 J.U.C的三個Executor接口

第三章 線程池的設計與實現

3.1 線程池的構造函數

3.2 線程池的執行步驟

3.3 線程池的狀態

3.4 生命週期與線程池大小選定 


第一章 基本概念

在web開發中,服務器需要接受並處理請求,所以會爲一個請求分配一個線程進行處理,如果併發的請求數量非常多,但每個線程執行的時間很短,這樣就會頻繁的創建和銷燬線程,如此一來,會大大降低系統的效率,可能會出現服務器在爲每個請求創建新線程和銷燬線程上花費的時間和消耗的系統資源要比處理實際的用戶請求的時間和資源更多,我們需要一種方法能夠重複的利用線程去完成新的任務.

一般利用Executors創建不同的線程池滿足不同的場景需求.

位於JUC包下的Executors目前提供了五種不同的線程池創建配置.

第五種方法是JDK8才引入的創建線程池的方法,我們下面大概講解一下ForkJoinPool

1.1 Fork/Join框架

此框架是java7提供的並行執行任務的框架.

總的來說是一個:

Fork/Join框架是ExecutorService接口的一種具體的實現,目的是爲了更好地利用多處理器帶來的好處,它是爲那些能遞歸的拆分成子任務的工作類型量身設計的,其目的在於能夠使用所有可用的運算能力來提升你的應用的性能,這點與mapreduce是一樣的,Fork/Join會將任務分發給線程池中的工作線程,它使用Work-Stealing算法.

Fork/Join將子任務放到不同的隊列裏,併爲每個隊列創建一個單獨的線程來執行隊列裏的任務,那麼這裏會出現一種情況,有些線程任務隊列的任務已經完成,有的隊列還有任務沒有完成,這就造成已完成任務線程會被閒置,爲了提高效率,完成自己任務而處於空閒的線程能夠從其它仍處於busy狀態的工作線程處竊取等待執行的任務.爲了減少竊取任務線程和被竊取任務線程間的競爭,通常會使用雙端隊列,被竊取任務線程永遠會從雙端隊列的頭部執行,而竊取任務的線程永遠從雙端隊列的尾部執行.

Work-Stealing算法:某個線程從其他隊列裏竊取任務來執行.

 

1.2 爲什麼要使用線程池

1.降低資源消耗,通過重複利用已創建的線程來降低線程創建和銷燬造成的消耗

2.提高線程的可管理性,線程是稀缺資源,重複創建增大系統的消耗與不穩定性.使用線程池可以進行統一的分配,調優和監控.

 

第二章 源碼理解

找到juc包中,發現幾個新建線程池的方法都會返回ThreadPoolExecutor,只是它們的參數是不一樣的.

public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }


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


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

點進ThreadPoolExecutor的源碼之中,發現其繼承鏈如下.

對於newSingleThreadScheduledExecutor,其返回是DelegatedScheduledExecutorService,

public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
        return new DelegatedScheduledExecutorService
            (new ScheduledThreadPoolExecutor(1));
    }

發現其繼承鏈仍包含ExecutorService.

所以這四類線程都起源於Executor,

查看ForkJoinPool的源碼,發現其本質上也實現了ExecutorService接口.

public static ExecutorService newWorkStealingPool(int parallelism) {
        return new ForkJoinPool
            (parallelism,
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

總的來說,它們都屬於Executor框架體系下的類或者接口.

Executor框架是一個根據一組執行策略調用調度執行和控制的異步任務的框架,目的是提供一種將任務提交與任務如何運行分離開來的機制.

2.1 J.U.C的三個Executor接口

Executor:運行新任務的簡單接口,將任務提交和任務執行細節解耦.

可以看到Executor中只有一個方法,對於不同Executor的實現,Executor方法可能是創建一個新線程並立即啓動,也可能是使用已有的工作線程來運行傳入的任務,也可能是根據設置線程池的容量或阻塞隊列的容量來決定是否要將傳入的線程放入阻塞隊列中或拒絕接收傳入的線程.

public interface Executor {
    void execute(Runnable command);
}

具體執行:

ExecutorService:擴展了Executor接口,添加了一些管理執行器生命週期和任務生命週期的方法,提供了更全面的提交任務機制.如返回future和無視void的submit方法.

回到源碼中,打開ExecutorService

可以看到其傳入的參數類型是Callable,Callable彌補了Ruuable無法返回結果的短板,submit較executor提供了更加完善的提交任務機制.

<T> Future<T> submit(Callable<T> task);

ScheduledExecutorService擴展了ExecutorService,同時支持Future和定期執行任務.

總的來說:java提供了上述三個接口的標準庫實現,如ThreadPoolExecutor,ScheduledThreadPoolExecutor,這些線程池的設計特點在於其高度的可調節性和靈活性,以儘量滿足複雜多變的實際應用場景,Excecutors則從簡化使用的角度爲我們提供了各種方便的靜態工廠的方法.

 

第三章 線程池的設計與實現

在大多數應用場景下,使用Executors提供的五類線程池就足夠了,但還是有些場景需要直接利用ThreadPoolExecutor等構造函數去創建.這就要求對線程構造方法有進一步的瞭解.

我們這裏對最重要的ThreadPoolExecutor進行分析.

應用提交任務到線程池去處理,然後再到線程池內部如何處理任務,再到處理完成,返回給應用的流程,可以看到線程池有一個工作隊列來接客,即存儲用戶提交的各個任務,隊列可以是SynchronousQueue<Runnable>,也可以是LinkedBlockingQueue,隊列接客完畢後,就會把任務提交給內部的線程池即工作線程的集合,該集合需要在運行的過程中管理線程的創建和銷燬,線程池的工作線程被抽象爲靜態內部類Worker,ThreadPool維護的就是Worker對象.

worker對象中的firstTask用它來保存傳入的任務,是在調用構造函數方法時通過ThreadFactory來創建出來的線程,因爲worker實現了Runnable接口也就是一個線程了,所以worker啓動的時候會調用worker裏的run方法去執行裏面的邏輯.

private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
        {
        //...
        final Thread thread;
        /** Initial task to run.  Possibly null. */
        Runnable firstTask;
        /** Per-thread task counter */
        volatile long completedTasks;
        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }
        public void run() {
            runWorker(this);
        }
        //...
}

在源碼中我們看到,ThreadFactory提供線程池所需的創建線程的邏輯,如果任務提交被拒絕,比如線程池已經處於關閉的狀態,此時新來的任務需要有處理機制來處理,Java標準庫裏面就提供了一些實現了RejectExecutionHandler接口的類供我們使用.

3.1 線程池的構造函數

構造線程池時有一些初始化參數如nthreads,threadFactory.

public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }

各個初始化參數的含義: 

默認使用的是dafaultThreadFactory,用其創建出來的線程會有相同的priority(優先級)並且是非守護線程,同時也設置了線程的名稱.

 handler參數:

3.2 線程池的執行步驟

新任務提交executae執行後執行的判斷

 

線程池與線程一樣也會有生命週期,生命週期也是通過狀態值來表示的,此外因爲線程池的作用就是用來管理線程的,那麼對線程的數量其肯定也是清楚的,也就是說會有個地方存儲當前有效的線程數,而ThreadPoolExecutor則將狀態值和有效線程數合二爲一存儲到了ctl中,ctl是對線程池的運行狀態和線程池中有效線程數量進行控制的一個字段,它主要包括兩部分的信息,線程池的運行狀態,線程池內有效線程的數量.ctl的高三位是用來保存runstate的,另外的29位是用來保存workcount的.

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    private static final int COUNT_BITS = Integer.SIZE - 3;
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

狀態的獲取:

runStateOf是用來獲取運行狀態的,workerCountOf主要是用來獲取活動線程數的,ctlOf是獲取兩者的,裏面用的都是與或非的操作,執行起來是相當高效且優雅的.

3.3 線程池的狀態

線程池有五種狀態

狀態轉換過程:

3.4 生命週期與線程池大小選定 

工作線程的生命週期:

線程池大小的選定:

 

 

 

 

 

 

 

 

 

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