好久没有写博客了,今天来写一个线程池的内容。
我们先来看一个简单地例子:
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(5);
executor.execute(new Running(1));
这段代码就是我们在使用线程池的时候最基本的一个使用方式了,而这里出现的几个类,ThreadPoolExecutor、Executors等,究竟分别是干什么用的呢?本着面向对象的原则,我们有必要去了解一下整个线程池的类族设计。
为了方便起见,在了解类族结构的时候,我们不直接读源码,采用阅读jdk api的方式,首先在源码中不断寻找父类和接口后,我们发现,其最高位的祖先为Executor接口,查看接口api,其解释如下:
这个接口的设计原则,顾名思义,就是Runnable的执行器,接口的设计者认为我们不应该使用new Thread。。。的方式去启动一个线程,而是采用Executor的实现类去启动,至于其原因,我相信对于那些对面向对象思想有一定理解的童鞋一定不会觉得难以理解,这种将线程的启动方式隐藏在实现类内部的设计,不但可以让线程的生命周期的处理变得统一,且对于不同处理方式的线程也可以分门别类,更重要的是,他可以让线程的启动和线程的执行业务完成抽象分离,交由不同的开发者去处理,这也就是为什么jdk(或者其他第三方库)设计的线程池(或者其他线程执行类)能够简单地用于我们自己开发的业务的线程的实际执行。
再来看Executor的子接口ExecutorService:
这里说明了两个问题:1.Service提供了管理终止的方法;2.Executors提供了关于这个接口的工厂方法。
第二点很好理解,我们在开头的例程中使用Executors去创建一个ThreadPoolExecutor,这就很明显是一个工程类了。
而第一点,官方也给出例程来解释:
class NetworkService implements Runnable {
private final ServerSocket serverSocket;
private final ExecutorService pool;
public NetworkService(int port, int poolSize)
throws IOException {
serverSocket = new ServerSocket(port);
pool = Executors.newFixedThreadPool(poolSize);
}
public void run() { // run the service
try {
for (;;) {
pool.execute(new Handler(serverSocket.accept()));
}
} catch (IOException ex) {
pool.shutdown();
}
}
}
class Handler implements Runnable {
private final Socket socket;
Handler(Socket socket) { this.socket = socket; }
public void run() {
// read and service request on socket
}
}
在这个例程中,对于execute操作发生异常的情况下,强制使用shutdown来关闭线程。
另外虽然官方文档没有强调,但是在ExecutorService中还设计了一系列面向Callable接口的方法。关于Callable和Runnable的区别,我们以后有机会再说。
继续看其实现类,一个抽象类AbstractExecutorService
这个类实现了一些基本的方法,但是并不实现具体的线程启动操作。
最后就是我们本次博客的主角,ThreadPoolExecutor了。
到目前为止,类族的结构应该已经看得比较清晰了,我们就来具体看一下,ThreadPoolExecutor的类结构吧。
这个类的构造方法比较多,由于不同的构造方法之间都是互相调用来调用去的,我们直接看参数最多的那个构造函数就可以啦。
看官方参数解释:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize
- 池中所保存的线程数,包括空闲线程。
maximumPoolSize
- 池中允许的最大线程数。
keepAliveTime
- 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
unit
- keepAliveTime 参数的时间单位。
workQueue
- 执行前用于保持任务的队列。此队列仅保持由 execute 方法提交的 Runnable 任务。
threadFactory
- 执行程序创建新线程时使用的工厂。
handler
- 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。
前四个参数应该不需要解释的吧,线程池嘛,当然要告诉他,有多少主要线程,不够用时候最多能有几个,排队最多排多久。
第五个参数workQueue,这个参数是任务队列,当线程池里面的任务跑满的情况下,会把新execute传入的那个Runnable扔到workQueue里面,然后当有线程跑完了以后,再去workQueue里面拿出来,然后。。。。反正就是这样嘛,这玩意就是排队用的,而且为了避免程序员自己去传入这个参数,通常工厂类Executors都直接把这个参数给屏蔽了,如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
工厂类直接帮我们传了个Queue进去,你就不用自己传了。
什么?你问为什么要用LinkedBlockingQueue?因为这玩意线程安全而且容易插入删除,毕竟嘛。。。。嗯
至于第六个参数threadFactory
,这个参数我感觉就是个酱油,唉简单说一说
static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
直接给你们看官方默认的生成方法,这里你们一定要搞清楚一个概念:
Runnable是个任务,Thread是个线程,这两玩意虽然常常一起用,但是是有概念上的区别的。
线程池可以无限的接收任务,但是线程数目有上限,如果线程满了,就让任务等待,任务是业务层传入的,但是线程是线程池自己创建的。就是这么个道理,既然要创建自然也需要一个创建的方式咯。(这里不得不说jdk源码的设计真的是精细,如果换做一般人,到这一步,这种可扩展的设计的情况下,默认估计就直接new Thread(r)了)。
handler
参数是指如果任务(Runnable)排队排太久了超出了时间限制,那就调用handler方法处理并将任务移除。handler就一个回调,没啥太大作用。
好了构造方法搞定了,那么Executors中自然也就是调用一下构造方法,按照我们开头的例程,代码已经一半搞定了,接下来我们就要开始看痛苦的后一半内容——execute方法了。
上代码吧:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();//获取当前线程数
if (workerCountOf(c) < corePoolSize) {
// 如果线程数量少于核心线程数量,就加入线程
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
// 如果线程数量已经大于核心线程数量,并且线程池正在运行(没有shutdown),那就把任务加入队列
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
// 如果加完线程池关了,那就拒绝
reject(command);
else if (workerCountOf(recheck) == 0)
// 如果正在运行的线程数量不对,就加一个运行的线程
addWorker(null, false);
}
else if (!addWorker(command, false))
// 如果任务队列数量已经满了,加不进去了,那只能尝试将任务加入工作池中,如果加不进去就拒绝
reject(command);
}
这里的拒绝reject就是调用一下之前构造器传入的那个handler。
execute方法的操作可以看清楚,对线程池处于不同情况下,做了个分类,如果觉得注释的不够清楚,我们再看一看官方的逻辑:
排队有三种通用策略:
SynchronousQueue
,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。LinkedBlockingQueue
)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize 的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web 页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。ArrayBlockingQueue
)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O 边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。然后我们来看一看他的提交任务的代码addWorker:
private boolean addWorker(Runnable firstTask, boolean core) {
// 通过一堆循环来给原子类加1,这个数表示线程池的线程数
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// 生成worker
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
// 运行线程
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
简单说明addWorker就是,先利用CAS原子类去给线程数+1,然后新生成一个Worker去启动线程。传入firstTask
然后这里我们看到一个w.thread 然后这玩意被start()了,我们看下worker的源码。
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
// ...
/** 工作线程 */
final Thread thread;
/**任务 */
Runnable firstTask;
/**
* Creates with given first task and thread from ThreadFactory.
* @param firstTask the first task (null if none)
*/
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
/** Delegates main run loop to outer runWorker. */
public void run() {
runWorker(this);
}
// ...
}
然后我们就发现,这个t.start()其实就是在线程中执行worker里的run(注意这个和传入的runnable是个父子关系)
然后最后:
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
循环,第一个任务处理完了,处理在队列中等待的。
后面就不解释了,把任务取出来,执行下pre,执行下run,执行下after完事。
获取等待队列的方式:
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
线程池虽然是个非常优秀的设计,但他并非万能,并非需要线程的地方就一定要用线程池,有些时候线程池反而会影响正常的功能。
在高运算的时候,可以使用线程池来限制线程的数目同时加快运算,但是在IO阻塞的时候,线程池就不应该被运用了,因为IO的读取并不符合线程池设计解决的问题。
当然我们也可以使用纯粹的线程IO读取完毕后将逻辑交给线程池处理,但是在http服务器日趋成熟,websocket被广泛运用的今天,这种架构的设计也就从与一般的开发人员牵扯不上关系了。但是了解底层的运行逻辑,兴许会在日常开发中帮助我们。
此外,在线程池中被运用到的,原子类、线程安全的队列等,我也会在未来的博客中进行解读。