前面分析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的邏輯很簡單,但是併發性能可是很不錯的。