Jetty源碼分析之線程池:QueuedThreadPool

前面分析Jetty整體架構的時候介紹過Jetty的三大組件:Acceptor、Handler和ThreadPool;前兩者工作的時候都是需要線程的,而所需的線程正是從ThreadPool中獲取的。這篇文件就是來分析ThreadPool的一個具體實現:QueuedThreadPool。下面是它的類圖:

這裏寫圖片描述

繼承了父類AbstractLifeCycle之後,QueuedThreadPool就可以當成一個LifeCycle類型的組件管理,這個父類在前面介紹生命週期的時候已經介紹過了,這裏就不重複介紹了。

ThreadPool是一個接口,裏面定義了一些操作和獲取線程信息的方法,它的完整定義如下:

public interface ThreadPool
{
    //將傳入的任務進行分派
    public abstract boolean dispatch(Runnable job);


    /**
     * Blocks until the thread pool is {@link LifeCycle#stop stopped}.
     */
    public void join() throws InterruptedException;

   //返回當前線程池中的總線程數目
    public int getThreads();

    //返回線程池中空閒的線程數量
    public int getIdleThreads();

    /**
     * @return True if the pool is low on threads
     */
    public boolean isLowOnThreads();

上面的幾個方法定義都很簡單,沒有什麼很難理解的方法。

SizedThreadPool是ThreadPool的一個內部接口,它在ThreadPool的基礎上增加了數量限制,可以認爲是一個數量有限的線程池,可以指定這個線程池中的最小和最大線程數。下面是其完整定義,可以看到也很簡單。

public interface SizedThreadPool extends ThreadPool
    {
        public int getMinThreads();//獲取限制的最小線程數量
        public int getMaxThreads(); //獲取限制的最大線程數量
        public void setMinThreads(int threads);//設置最小線程數量
        public void setMaxThreads(int threads);//設置最大線程數量
    }

Executor是java.util.concurrent包下的一個接口,接口中只有一個方法如下:

public interface Executor {

    void execute(Runnable command);
}

execute()方法接受一個Runnable類型的對象,然後負責在一個線程中執行這個task。至於這個線程就是當前調用execute()方法的線程還是另外分配的一個線程,都是由具體實現的子類決定的。這種方式的好處在於調用者不用再自己創建線程,線程的管理完全都有Executor的子類負責。顯然QueuedThreadPool是很適合這種場景的。

上面分析完繼承的父類之後就發現定義都比較簡單,沒有什麼特別難理解的方法或者調用關係。所以下面來分析QueuedThreadPool中是如何實現這幾個父類中的方法的。

1) doStart()方法
doStart()方法時在容器啓動的時候就會被調用的一個方法,QueuedThreadPool和其它組件一樣在這個方法中進行初始化。

   @Override
    protected void doStart() throws Exception
    {
        //super.doStart()會調用AbstractLifeCycle的doStart()方法,
        //而那個方法中是沒有做任何事情的。
        super.doStart();
        //將啓動的線程數設置爲0
        _threadsStarted.set(0);
        //_jobs是QueuedThreadPool用來存儲傳入Runnable對象的數據結構,可以在jetty的xml配置文件中指定具體類型,如果沒有指定則會根據_maxQueued屬性的值來選擇,具體如下。
        if (_jobs==null)
        {
            _jobs=_maxQueued>0 ?new ArrayBlockingQueue<Runnable>(_maxQueued)
                :new BlockingArrayQueue<Runnable>(_minThreads,_minThreads);
        }

        //如果當前啓動的線程數小於設定的最小值,則不斷啓動新的線程,
        //直到線程池中有_minThreads個線程
        int threads=_threadsStarted.get();
        while (isRunning() && threads<_minThreads)
        {
            startThread(threads);//startThread方法負責啓動新線程
            threads=_threadsStarted.get();
        }
    }

上面這段代碼邏輯很簡單,需要注意的幾個點如下:

  • QueuedThreadPool中有兩個屬性來記錄當前已啓動和空閒的線程數,它們都是線程安全的AtomicInteger類型變量,如下:
private final AtomicInteger _threadsStarted = new       AtomicInteger(); //記錄已啓動的線程數量
    private final AtomicInteger _threadsIdle = new AtomicInteger(); //記錄空閒的線程數量 
  • ArrayBlockingQueue是java.util.concurrent包下用數組實現的阻塞隊列,性能不是很好。而BlockingArrayQueue是jetty內部基於循環數據實現的一個阻塞隊列,性能會好一點。

下面來看下startThread(int threads)方法中的邏輯:

private boolean startThread(int threads)
    {
        final int next=threads+1;
        //原子變量的先比較後更新操作,如果比較失敗,說明有其它線程在併發操作線程池,這種情況下如果不返還則會導致_threadsStarted記錄的啓動線程數目出錯。如果將整個startThread()方法都進行加鎖是可以避免這種情況的,但是那樣的話會極大的降低併發性。
        if (!_threadsStarted.compareAndSet(threads,next))
            return false;
        boolean started=false;
        try
        {
            Thread thread=newThread(_runnable);
            thread.setDaemon(_daemon);
            thread.setPriority(_priority);
            thread.setName(_name+"-"+thread.getId());
            _threads.add(thread);

            thread.start();
            started=true;
        }
        finally
        {
            if (!started)
                _threadsStarted.decrementAndGet();
        }
        return started;
    }

整個方法就是新建一個線程並啓動然後將其加入到線程池_threads中。_threads是QueuedThreadPool中用來存儲線程對象的容器,是一個無限容量的隊列。

    private final ConcurrentLinkedQueue<Thread> _threads=new ConcurrentLinkedQueue<Thread>();

下面來重點關注下創建線程的時候,傳遞給線程的Runnable對象_runnable中的邏輯。這個對象的定義如下:

  private Runnable _runnable = new Runnable()
    {
        public void run()
        {
            boolean shrink=false;
            try
            {  
                //從任務隊列中取出一個任務來,poll()是非阻塞的操作,沒有元素時返回null
                Runnable job=_jobs.poll();
                while (isRunning())
                {
                    // 如果任務隊列中一直有任務,則不斷取出其中的任務執行
                    while (job!=null && isRunning())
                    {
                        runJob(job);//其實就是調用job.run()方法
                        job=_jobs.poll();
                    }

                    // 任務隊列中沒有任務時的空閒循環
                    try
                    {
                        //增加空閒線程的數量
                        _threadsIdle.incrementAndGet();

                        while (isRunning() && job==null)
                        {
                            //將空閒等待時間的設爲不大於0的情況下(默認是1分鐘),則當前線程會一直阻塞在任務隊列的take()操作上.
                            if (_maxIdleTimeMs<=0)
                                job=_jobs.take();
                            else
                            {
                                // 下面是是否要收縮線程池的判斷
                                final int size=_threadsStarted.get();//目前以窮的線程數
                                if (size>_minThreads)
                                {
                                    long last=_lastShrink.get(); //上次進行收縮線程池操作的時間,未進行過則爲0
                                    long now=System.currentTimeMillis();
                                    if (last==0 || (now-last)>_maxIdleTimeMs)
                                    {

//最後設置_lastShrink和_threadsStarted的數目,並且使用的都是原子變量的compareAndSet類型操作,防止併發修改的問題。                         shrink=_lastShrink.compareAndSet(last,now) &&
                                        _threadsStarted.compareAndSet(size,size-1);
                                        //如果確實可以收縮
                                        if (shrink)
                                            return;
                                    }
                                }
                                //如果不滿足收縮的條件,則通過_jobs.poll(_maxIdleTimeMs,TimeUnit.MILLISECONDS)方法阻塞_maxIdleTimeMs,如果再次期間拿到job則返回job循環,否則再進行一次空閒循環的檢查。
                                job=idleJobPoll();
                            }
                        }
                    }
                    finally
                       //移除成功,減少空閒線程計數
                        _threadsIdle.decrementAndGet();
                    }
                }
            }
            catch(InterruptedException e)
            {
                LOG.ignore(e);
            }
            catch(Exception e)
            {
                LOG.warn(e);
            }
            finally
            {//進入這個finally有兩種情況,第一種是收縮線程池了(shrink爲true),另一種是異常退出了或改變服務器狀態退出了,這種情況下需要減少已啓動線程計數。
                if (!shrink)
                    _threadsStarted.decrementAndGet();
                 //直接將當前線程對象從線程池中移除,隨後會被垃圾回收。
                _threads.remove(Thread.currentThread());
            }
        }
    };

首先可以看到線程池和外部沒有直接進行交互(並不是從線程池中取出Thread對象給外部使用);而是通過_jobs這麼個阻塞隊列和外部進行交互,具體來說,外部有任務需要安排一個線程執行的時候就將任務加入到隊列中(如果隊列已滿則會添加失敗),而線程池中的線程會每隔一段時間就檢查一次任務隊列,如果有任務需要執行則會取出進行執行(job循環)。對於隊列中沒有任務需要處理的情況,可以通過設置_maxIdleTimeMs的值來控制線程的表現:如果_maxIdleTimeMs的值小於0,則線程會一直阻塞在_jobs.take()方法上;如果_maxIdleTimeMs的值大於0,則會先檢查是否可以收縮線程池(檢查的標準就是上次收縮的時間到目前要大於_maxIdleTimeMs並且當前啓動的線程數目大於_minThreads),如果可以收縮則當前線程會被從線程池中移除,如果不可以則當前線程會在_jobs.poll()方法上阻塞_maxIdleTimeMs時間,如果在這段時間裏這個方法返回一個job,則進行入job循環,否則繼續上面的循環。

2) dispatch()和execute()方法
這兩個方法都是線程池對外提供的執行方法,接受的參數都是一個Runnable對象。實際上execute()方法是通過dispatch()方法實現的:

public void execute(Runnable job)
    {
        if (!dispatch(job))
            throw new RejectedExecutionException();
    }

可以看到是直接調用的dispatch()方法。dispatch()方法的源碼如下:

public boolean dispatch(Runnable job)
    {
        if (isRunning())
        {
            final int jobQ = _jobs.size();
            final int idle = getIdleThreads();
            if(_jobs.offer(job)) //offer是非阻塞操作,如果底層隊列空間不夠,則立即返回false
            {
                //如果沒有空閒線程或當前隊列中等待的任務數大於空閒的線程數,並且線程池容量還沒達到_maxThreads的時候會新增一個處理線程。
                if (idle==0 || jobQ>idle)
                {
                    int threads=_threadsStarted.get();
                    if (threads<_maxThreads)
                        startThread(threads);
                }
                return true;
            }
        }
        LOG.debug("Dispatched {} to stopped {}",job,this);
        return false;
    }

3) setMinThreads()方法
最後看一下setMinThreads()方法:

public void setMinThreads(int minThreads)
    {
        _minThreads=minThreads;

        if (_minThreads>_maxThreads)
            _maxThreads=_minThreads;

        int threads=_threadsStarted.get();
        while (isStarted() && threads<_minThreads)
        {
            startThread(threads);
            threads=_threadsStarted.get();
        }
    }

這個方法就幾行代碼,值得注意的是在啓動之後可以通過擴大_minThreads的值來實現線程池的動態擴大。

上面幾個方法分析完,QueuedThreadPool也就算分析完了,說實話QueuedThreadPool的邏輯比前面那些Handler簡單多了,所以分析源碼也輕鬆很多。不過雖然QueuedThreadPool的邏輯很簡單,但是併發性能可是很不錯的。

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