之前Java線程池的文章都是關於基本知識和JUC下的類。這篇主要來說一下線程池,並自己來實現一個線程池。
一.線程池介紹
學習過程中會遇到各種池,有線程池,數據庫連接池,內存池,常量池等等。下面來一次介紹。
線程池: 用來管理線程的一個集合(池),作用是用來提高線程的使用效率。如果一個線程的創建和銷燬的成本比運行該線程裏面的程序的成本要高,則就需要用到線程池。在線程池中,裏面的線程可以重用,而不用每一個任務都創建一個線程。Java5引入了Executors類產生線程池。
數據庫連接池: 程序要和數據庫通信就需要建立一個連接,這個連接的建立和關閉時及其耗費系統資源,數據庫連接池就是用來管理這些連接的。它的原理和線程池一樣,只不過線程換成了連接。我之前使用過的C3P0連接池,它是一個開源的JDBC連接池,Hibernate和Spring使用它。
內存池: 內存在C/C++上是手動管理的,對象的新建和銷燬是要手動在內存堆上分配和釋放,這個會消耗系統資源,如果應用程序頻繁地在堆上分配和釋放內存,則會導致性能的損失。並且會使系統中出現大量的內存碎片,降低內存的利用率。這是就要用到內存池來對內存的分配和釋放進行管理。由於Java是自動管理內存的,所以沒有辦法實現內存池。
常量池: 常量池是JVM的一塊特殊的內存空間,屬於Java內存中的方法去,用於存放常量(8種基本)和符號引用。它同上面三種不是一類的。
二.Java線程池類
Java5開始,java內建支持線程池。JUC包下的Executors工廠類(工具類)用來產生線程池。使用它的子接口來定義線程池,通過靜態方法來創建不同的線程池。有固定數目大小的線程池,有可延遲執行的線程池,有具有緩存功能的線程池等等。兩個子接口ExecutorService,ScheduledExecutorService繼承了接口Executor。
下面我們來看一下java內部是如何實現線程池源碼的。這裏就拿個簡單的固定線程池來說明,其他類型的也差不多。下面是一個創建並運行線程池的例子:
public static void main(String [] args){
ExecutorService pool=Executors.newFixedThreadPool(5);//創建具有5個固定線程的線程池
pool.submit(new MyThread()); //提交任務,MyThread是一個實現Runnable接口或者Callable接口的類
pool.submit(new MyThread());
}
首先是創建一個ExecutorService線程池,我們看看源碼是:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
它其實是調用了ThreadPoolExecutor類,我們也可以直接創建這個類。它的構造方法如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
創建固定的線程數參數nThreads,在上面是將corePoolSizes和maximumPoolSize都設爲相同的值。workQueue是一個任務隊列,它是用來放需要執行的任務,任務是要實現Runnable接口的,它的實現是LinkedBlockingQueue<Runnable>(),它叫鏈表阻塞隊列,意思是在獲取任務時,如果裏面沒有任務,就會造成阻塞。threadFactory是用來創建Thread線程的。其他的參數在這裏沒啥作用。再來看看ExecutorService.submit()方法,它用來提交任務。它其實是調用了它實現類AbstractExecutorService的submit方法:
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Object> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
它創建了一個FutureTask類,將提交的任務task放在了FutureTask類中,這個類上一篇文章說過,就是在線程中執行任務。然後將執行的任務調用execute()方法。下面看execute方法。這個方法,在ThreadPoolExecutor實現。它的定義是這樣的public class ThreadPoolExecutor extends AbstractExecutorService。它纔是整個線程池的關鍵類。下面看execute()方法:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
if (runState == RUNNING && workQueue.offer(command)) {
if (runState != RUNNING || poolSize == 0)
ensureQueuedTaskHandled(command);
}
else if (!addIfUnderMaximumPoolSize(command))
reject(command); // is shutdown or saturated
}
}
第一個if沒啥好解釋,如果任務是null直接拋出異常。
第二個if裏面有兩個判斷,第一個判斷:poolSize>=corePoolSize 它們是int類型,如果當前的線程數大於定義的core線程數;或者 第二個判斷:(主要如果第一個判斷是true則不用進行第二個判斷),就是噹噹前線程數小於定義的線程數時,才調用addIfUnderCorePoolSize()方法,一開始,當前線程數是小於定義的線程數,所以來看看addIfUnderCorePoolSize方法:
private boolean addIfUnderCorePoolSize(Runnable firstTask) {
Thread t = null;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (poolSize < corePoolSize && runState == RUNNING)
t = addThread(firstTask);
} finally {
mainLock.unlock();
}
return t != null;
}
裏面還用到了Lock類進行同步處理,它裏面又進行了一次線程輸入判斷,當運行狀態爲RUNNING時,才調用addThread(firstTask)方法,它是創建任務線程的方法,也就是在addThread方法中創建Thread類,我們來看方法:
private Thread addThread(Runnable firstTask) {
Worker w = new Worker(firstTask);
Thread t = threadFactory.newThread(w);
boolean workerStarted = false;
if (t != null) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
w.thread = t;
workers.add(w);
int nt = ++poolSize;
if (nt > largestPoolSize)
largestPoolSize = nt;
try {
t.start();
workerStarted = true;
}
finally {
if (!workerStarted)
workers.remove(w);
}
}
return t;
}
裏面有一個類Worker:任務的執行者,它是ThreadPoolExecutor類的一個內部類,用於執行任務,它將Thread作爲它的一個屬性。然後將定義的w對象加入到workers中。workers是一個HashSet<Worker>類,它用來存放Worker。然後將新建的Thread開啓,用了start()。Thread任務執行的是w的run()方法。Worker的run方法如下:
public void run() {
try {
hasRun = true;
Runnable task = firstTask;
firstTask = null;
while (task != null || (task = getTask()) != null) {
runTask(task);
task = null;
}
} finally {
workerDone(this);
}
}
它裏面是一個while循環,先從firstTask運行,如果爲null,則調用getTask()方法,獲取任務。然後在用runTask方法執行任務。getTask方法如下:
Runnable getTask() {
for (;;) {
try {
int state = runState;
if (state > SHUTDOWN)
return null;
Runnable r;
if (state == SHUTDOWN) // Help drain queue
r = workQueue.poll();
else if (poolSize > corePoolSize || allowCoreThreadTimeOut)
r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);
else
r = workQueue.take();
if (r != null)
return r;
if (workerCanExit()) {
if (runState >= SHUTDOWN) // Wake up others
interruptIdleWorkers();
return null;
}
// Else retry
} catch (InterruptedException ie) {
// On interruption, re-check runState
}
}
它也是一個循環,關鍵是這句 r = workQueue.take(),就是從阻塞隊列中獲取任務並在隊列中刪除獲取的任務(它保證了線程的安全),如果有則獲取成功,結束for循環返回,如果隊列裏面沒有任務則阻塞,直到裏面有新任務才返回。然後就執行runTask(task)這就,它纔是真正執行任務的程序。runTask裏面的關鍵代碼如下:
try {
task.run();
ran = true;
afterExecute(task, null);
++completedTasks;
} catch (RuntimeException ex) {
if (!ran)
afterExecute(task, ex);
throw ex;
}
是調用了任務的run方法。這樣一個任務就完成了。 Worker的workerDone方法表明,如果這個任務執行者結束了while循環,代表了這個線程會被刪除,就由workerDone方法來完成。這就是一個基本的線程池實現。
再來說一下,線程池就是開啓了多個線程,把它們放入到HashSet中,需要執行的任務放在Queue隊列中,多個線程就不斷的獲取隊列中的任務進行執行。用Lock類來確保數據的安全性,如線程池的數量,各個int類型的參數,在獲取執行任務時並沒有加鎖,而是用到了安全的阻塞隊列,它是LinkedBlockingQueue<Runnable>(),它確保調用的take()方法是安全的,因此多個線程在獲取任務的時候不會發生重讀和死鎖。
三.自己編寫Java線程池
通過上面對java自帶線程池源碼的分析,知道了線程池的內部構造,這樣自己寫一個線程池也就不難了。我自己就實現一個簡單的有固定數目的線程池。線程池採用數組裏存放線程,採用LinkedList存放任務,獲取任務的時候使用synchronized進行同步處理確保線程安全。任務執行線程MyThreadPool繼承了Thread,這樣就不用把Thread類作爲屬性了,代碼如下:
通過上面對java自帶線程池源碼的分析,知道了線程池的內部構造,這樣自己寫一個線程池也就不難了。我自己就實現一個簡單的有固定數目的線程池。線程池採用數組裏存放線程,採用LinkedList存放任務,獲取任務的時候使用synchronized進行同步處理確保線程安全。任務執行線程MyThreadPool繼承了Thread,這樣就不用把Thread類作爲屬性了,代碼如下:
class MyThreadPool{
private int threadsize; //線程的個數
private MyThread[] threadpool; //存放線程的線程池
private LinkedList<Runnable> queue; //任務隊列
private boolean state=true; //true代表線程池是開啓的,如果是false則是關閉的
public MyThreadPool(int size){
threadsize=size;
queue=new LinkedList<Runnable>();
threadpool=new MyThread[threadsize];
for(int i=0;i<size;i++){
threadpool[i]=new MyThread();
threadpool[i].start(); //創建完之後隨即開啓線程
}
}
public void submit(Runnable task){ //提交任務,就是向隊列中添加任務
synchronized(queue){
queue.add(task);
queue.notify();
}
}
public void shutdown(){ //關閉線程池,需要同步,並調用notefyAll方法喚醒阻塞的線程
synchronized(queue){
state=false;
queue.notifyAll();
}
}
private class MyThread extends Thread{
private Runnable task;
@Override
public void run(){
while(state){ //當state是true時才運行,否則結束
synchronized(queue){
while(queue.isEmpty() && state){
try{
queue.wait();//阻塞該線程,直到有新任務加入隊列或者關閉線程池
}
catch(Exception e){
e.printStackTrace();
}
}
if(!queue.isEmpty()) //再判斷一次queue是否有數據,否則可能出現NoSuchElementException錯誤
task=queue.pop();
} //結束同步互斥
if(task!=null && state){
task.run(); //運行任務
}
}
}
}
}
下面對自己創建的線程池進行測試,代碼如下: 執行10000次的自增操作。第一個就創建了10000個線程,每個線程加1,第二個創建了有20個線程的線程池來完成。對比結果,線程池的作用還是蠻明顯的。
public class di implements Runnable
{
private static int count=0;
public synchronized void run(){
count++;
}
public static void main(String[] args) throws IOException{
//下面來對我自建的線程池進行測試。用對比實驗
//首先是沒用線程池的情況,創建10000個線程,每個線程完成自增1的任務,直到對象的count=10000
di test1=new di();
long start=System.currentTimeMillis();
for(int i=0;i<10000;i++){
new Thread(test1).start();
}
long end=0;
while(true){
if(test1.count>=10000){
end=System.currentTimeMillis();
System.out.println("沒使用線程池,花費時間:"+(end-start));
break;
}
}
//採用線程池
di test2=new di();
MyThreadPool my=new MyThreadPool(20);
start=System.currentTimeMillis();
for(int i=0;i<10000;i++){
my.submit(test2);
}
while(true){
if(test2.count>=10000){
end=System.currentTimeMillis();
System.out.println("使用線程池,花費時間:"+(end-start));
break;
}
}
my.shutdown();
}
}
結果: