提升工作效率利器:
Mac App Store 上的“Whale - 任务管理、时间、卡片、高效率”
1 简介
线程的使用在java中占有极其重要的地位,在jdk1.4极其之前的jdk版本中,关于线程池的使用是极其简陋的。在jdk1.5之后这一情况有了很大的改观。Jdk1.5之后加入了java.util.concurrent包,这个包中主要介绍java中线程以及线程池的使用。为我们在开发中处理线程的问题提供了非常大的帮助。
2 线程池的作用
线程池作用就是限制系统中执行线程的数量。
根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。
为什么要用线程池:
1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存(具体大小可以通过设置线程栈来修改),线程开的越多,消耗的内存也就越大,最后死机)。
Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。
比较重要的几个类:
ExecutorService | 真正的线程池接口。 |
ScheduledExecutorService | 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。 |
ThreadPoolExecutor | ExecutorService的默认实现。 |
ScheduledThreadPoolExecutor | 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。 |
3 通过解读JDK中ThreadPoolExecutor的源码来深入了解Java中线程池的实现机制。
1)在JDK中,ThreadPoolExecutor类的大致结构如下图所示:
图1:
ThreadPoolExecutor包括了一个Worker集合和一个工作队列(workQueue)。Worker集合用来管理所有创建的Worker,工作队列(workerQueue)存放需要执行的任务。
通过ExecutorService executor = new ThreadPoolExecutorEx(8,10,60L,TimeUnit.SECONDS,new LinkedBlockingQueue<Runnable>())来创建一个corePoolSize=8,maximumPoolSize=10,keepAliveTime=60s的线程池。
corePoolSize:线程池维护的核心线程数。来任务时,只要线程池中当前的线程总数小于corePoolSize就创建新线程来处理新添加进来的任务。
maximumPoolSize:线程池维护的最大线程数。来任务时,如果任务队列里的任务已经达到了任务队列的最大允许个数并且线程池中可用的线程总数大于corePoolSize小于maximumPoolSize时,创建一个新线程来处理新添加进来的任务。
通过ThreadPoolExecutor的execute(task)方法来添加一个任务。
2)创建一个线程池,并往线程池里添加一个任务的执行步骤如下图所示:
图2:
步骤一:调用ThreadPoolExecutor类的execute(task)方法往线程池里添加一个线程,这是我们调用的方法,JDK源码如下:
代码(1)
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
}
}
如上代码所示,往线程池里添加一个任务时,如果线程池里当前的线程数poolSize小于corePoolSize的时候,直接调用addIfUnderCorePoolSize(command)方法来创建一个新的Worker。
当线程池里创建的线程个数达到了corePoolSize时,此时的任务添加到了workQueue里面,代码是workQueue.offer(command)。下面代码就是addIfUnderCorePoolSize(command)的源码:
代码(2)
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();
}
if (t == null)
return false;
t.start();
return true;
}
顺着代码(2)查看t = addThread(firstTask)方法的源码如下:
代码(3)
private Thread addThread(Runnable firstTask) {
Worker w = new Worker(firstTask);
Thread t = threadFactory.newThread(w);
if (t != null) {
w.thread = t;
workers.add(w);
int nt = ++poolSize;
if (nt > largestPoolSize)
largestPoolSize = nt;
}
return t;
}
从代码(3)我们可以看到,期间使用firstTask任务创建了一个线程Worker w对象,使用w对象创建了一个线程Thread t。并通过代码(2)里的t.start()来启动一个worker线程。start()方法调用之后,只要等该线程分配到CPU时间片,我worker类的run()方法就会被调用,继而我们的第一个任务firstTask的run()方法就会被执行了。
我们前面说过,当线程池里创建的线程个数达到了corePoolSize时,此时的任务添加到了workQueue里面,代码是workQueue.offer(command)。那么添加到workQueue队列里的线程什么时候被执行呢?
再看一下上面的第二张图,照样贴出源码如下:
代码(4)
public void run() {
try {
Runnable task = firstTask;
firstTask = null;
while (task != null || (task = getTask()) != null) {
runTask(task);
task = null;
}
} finally {
workerDone(this);
}
}
代码(4)是Worker类里的run()方法,Worker类是一个实现了Runnable的线程类。通过代码(2)里的t.start()来启动一个worker线程。start()方法调用之后,只要等该线程分配到CPU时间片,我worker类的run()方法就会被调用,继而我们的第一个任务firstTask的run()方法就会被执行了。为什么说继而任务的run()方法就会被执行了呢?
顺着代码(4)循环继续往下看runTask(task)方法的源码实现:
代码(5)
private void runTask(Runnable task) {
final ReentrantLock runLock = this.runLock;
runLock.lock();
try {
/*
* Ensure that unless pool is stopping, this thread
* does not have its interrupt set. This requires a
* double-check of state in case the interrupt was
* cleared concurrently with a shutdownNow -- if so,
* the interrupt is re-enabled.
*/
if (runState < STOP &&
Thread.interrupted() &&
runState >= STOP)
thread.interrupt();
/*
* Track execution state to ensure that afterExecute
* is called only if task completed or threw
* exception. Otherwise, the caught runtime exception
* will have been thrown by afterExecute itself, in
* which case we don't want to call it again.
*/
boolean ran = false;
beforeExecute(thread, task);
try {
task.run();
ran = true;
afterExecute(task, null);
++completedTasks;
} catch (RuntimeException ex) {
if (!ran)
afterExecute(task, ex);
throw ex;
}
} finally {
runLock.unlock();
}
}
代码(5)里面调用了task的run()方法,任务的内容就得到了执行。
任务执行完之后呢?当然是在没有任务的时候,释放线程资源,也就是释放掉worker。看步骤二。
步骤二:
在代码(4)的run()方法里,我们看到finally的代码块里执行workDone(this),也就是说当worker发现当前没有可执行的任务时,就将尝试结束自己,以释放掉占用的资源。看如下代码(6):
void workerDone(Worker w) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
completedTaskCount += w.completedTasks;
workers.remove(w);
if (--poolSize == 0)
tryTerminate();
} finally {
mainLock.unlock();
}
}
从代码(6)中可以看出,当前的worker从workers集合中删除掉了,忘记了workers集合请看第一张图^_^。从workers集合中删除后,就没有指向该worker的引用了,JVM会在适当的时候回收该worker。从集合中删除了该worker是否代表着线程池中从此少了一个线程呢,因为我们看到了poolSize--了?当线程池中的可用线程减少到0时,而恰巧在执行workDone(w)方法之前又来了一个任务被添加workQUeue中,这个任务是不是就没有可用的线程来执行了?这种情况确实会有,如何避免呢?顺着代码(6)继续往下看tryTerminate()方法的源码:
代码(7)
private void tryTerminate() {
if (poolSize == 0) {
int state = runState;
if (state < STOP && !workQueue.isEmpty()) {
state = RUNNING; // disable termination check below
Thread t = addThread(null);
if (t != null)
t.start();
}
if (state == STOP || state == SHUTDOWN) {
runState = TERMINATED;
termination.signalAll();
terminated();
}
}
}
tryTerminate()方法里的注释写的很清楚,如果线程池里可用的线程数到0,并且线程池的状态不是STOP或者TERMINATED,并且workQueue里还有任务时,创建一个线程,保证线程池中至少有一个线程可以处理任务。
在这里总结一下,ThreadPoolExecutor是一个高效的线程池实现,其机制是往线程池里添加任务时才创建线程,如果任务不多,执行任务不频繁时,使用ThreadPoolExecutor就是来一个任务创建一个线程,任务执行完销毁一个线程。但是在任务多,任务执行频繁时,正常情况下是需要频繁创建与销毁线程,ThreadPoolExecutor在任务多且执行频繁的实现机制是一个线程可以为多个任务工作,直到线程池的工作队列中没有剩下可执行的工作时才销毁线程。