实际开发中,并发和多线程的情景处处存在。今天我们来聊聊对象池和线程池这个两个日常开发用的比较多,但是又很容易混淆的两个概念。
对象池
首先我们来说说对象池。对象池利用了串行封闭的概念:将对象O“借给”一个请求线程T1,T1使用完之后再交还给对象池,并保证“未擅自发布该对象”且“以后不再使用”;对象池收回对象O之后,等T2来借的时候再把他借给T2,完成对象所用权的传递。
下面是一个简化版的对象池
public abstract class AbstractObjectPool<T> {
protected final int min;
protected final int max;
protected final List<T> usings = Lists.newLinkedList();
protected final List<T> buffer = Lists.newLinkedList();
private volatile boolean inited = false;
public AbstractObjectPool(int min, int max) {
this.min = min;
this.max = max;
if (this.min < 0 || this.min > this.max) {
throw new IllegalArgumentException(String.format("need 0<=min<=max<=Integer.MAX_VALUE,given min%s,max:%s", this.min, this.max));
}
}
public void init() {
for (int i = 0; i < min; i++) {
buffer.add(newObject());
}
inited = true;
}
public void checkInited() {
if (!inited) {
throw new IllegalStateException("not inited");
}
}
abstract protected T newObject();
public synchronized T getObject() {
checkInited();
if (usings.size() == max) {
return null;
}
if (buffer.size() == 0) {
T newObject = newObject();
usings.add(newObject);
return newObject;
}
T oldObject = buffer.remove(0);
usings.add(oldObject);
return oldObject;
}
public synchronized void freeObject(T object) {
checkInited();
if (!usings.contains(object)) {
throw new IllegalArgumentException(String.format("object not in using queue:%s", object));
}
usings.remove(usings.indexOf(object));
buffer.add(object);
}
}
AbstractObjectPool具有以下特性:
- 支持设置最小最大容量
- 对象一旦申请就不再释放,避免了GC
虽然简单,但是大可以用于一些时间敏感、资源充裕的场景。如果时间进一步敏感,可将getObject()、freeObject()改写为并发程度更高的版本,但是记得得保证安全发布安全回收;如果资源不那么充裕,可以适当增加对象的回收策略。
可以看到,一个对象池的基本行为包括:
- 创建对象newObject()
- 借取对象getObject()
- 归还对象freeObject()
典型的对象池有各种连接池(如数据库连接池)、常量池等。那对象池和线程是之间有什么区别呢,线程池的核心原理是不是对象池,下面我们来追溯一下java源码。
线程池
首先摆出结论:线程池糅合了对象池模型,但是核心原理是生产者-消费者模型。
继承结构如下:
用户可以将Runnable(或Callables)实例提交给线程池,线城池会异步执行该任务,返回响应的结果(完成/返回值)。
我最喜欢的是submit(Callable<T> task)方法。我们从该方法入手,逐步深入函数栈,探究线程池的实现原理。
submit()
submit()方法在ExecutorService接口中定义,在抽象类AbstractExecutorService实现,ThreadPoolExecutor直接继承。
public abstract class AbstractExecutorService implements ExecutorService {
...
/**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
}
AbstractExecutorService #newTaskFor()创建一个RunnableFutrue类型的FutureTask。核心是execute()方法
execute()方法
execute()方法在接口Executor定义,在ThreadPoolExecutor实现。
public class ThreadPoolExecutor extends AbstractExecutorService {
...
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
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);
}
...
}
我们暂且忽略线程池的池化策略。关注一个最简单的场景,看看线程池中任务是怎样执行的。
核心是addWorker()方法。以第8行为例,此时,线程池中线程数未达到最小线程池的大小corePoolSize,通常可以直接在第9行返回。
addWorker()
addWorker()方法是ThreadPoolExecutor的私有方法。
public class ThreadPoolExecutor extends AbstractExecutorService {
....
private boolean addWorker(Runnable firstTask, boolean core) {
...
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN){
workers.add(w);
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
....
}
这里去掉了很多用于管理线程池、维护线程安全的代码。假设线程池未关闭,worker(即w,下同)添加成功,则必然将worker添加至workers中。workers是一个HashSet:
/**
* Set containing all worker threads in pool. Accessed only when
* holding mainLock.
*/
private final HashSet<Worker> workers = new HashSet<Worker>();
哪里是对象池
如果说与对象池有关,那么只有workers及相当于对象池实例代码中的using,应用了对象池模型;只不过这里的using是一直增长的,直到打到最大线程池的大小maximumPoolSize。
但是很明显,线程池并没有将线程发布出去,workers仅仅是完成using保存线程的作用。那么线程池中的任务是怎样执行的呢?跟线程池有没有关系呢?
哪里又不是对象池?
注意上面addWorker()代码中的第9、17、24行代码
- 9行将我们提交到线程池的firstTask封装入一个worker.
- 17行将worker加入到workers维护起来
- 24行启动了worker中的线程t
核心就在这三行,线程池没有直接在addWorker()中启动我们传进来的firstTask,代之以启动 一个worker,最终任务必然被启动,那么我们继续看Worker是怎样启动我们这个任务的。
Worker
Worker实现了Runnable接口:
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
...
/**
* 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);
}
...
}
为什么要将构造Worker时的参数命名为firstTask?因为当且仅当需要建立新的Worker以执行任务task时,才会调用构造函数。因此,任务task对于新Worker而言,是第一个任务firstTask。
Worker的实现非常简单:将自己作为Runable实例,构造时在内部创建并持有一个线程thread。Thread和Runable的使用大家很熟悉了,核心是Worker的run方法,它直接调用了runWorker()方法。
runWorker()
重点来了。简化如下:
public class ThreadPoolExecutor extends AbstractExecutorService {
...
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();
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);
}
}
...
}
我们在前面将要执行的任务赋值给firstTask,5-6行首先取出任务task,并将firstTask置为null。因为接下来要执行task,firstTask字段就没有用了。
重点是10-31行的while循环。下面分情况讨论。
case1:第一次进入循环,task不为null
case1对应前面所有假设,程序正常运行进入到这个while循环。
第一次进入循环时,task==firstTask,不为null,直接进入循环体;从而执行16行的firstTask的run()方法;
异常处理我们在这里先不考虑;最后,在finally代码块中,task被置为null,导致下一轮循环会进入case2。
case2:非第一次进入循环体,task为null
case2是更为普遍的情况,是线程池的核心。
case1中,task被置为null,使得第10行的布尔表达式执行第二部分:(task = getTask()) != null) (注:getTask()方法稍后再讲,它返回一个用户已提交的任务)。假设task得到一个已提交的任务,从而16行执行的是新获得的新任务的run()方法。后面通case1,最后task也会被置为null,以后循环都进入case2。
getTask()
case2中每次循环都要获取一个新的已提交的任务,那任务从哪来呢?简化如下
public class ThreadPoolExecutor extends AbstractExecutorService {
...
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
...
}
我们先看简单的7-11行。
首先,workQueue是一个线程安全的BlockingQueue,大部分时候使用的实现类是LinkedBlockingQueue。
private final BlockingQueue<Runnable> workQueue;
假设timed为 false,则调用阻塞的take()方法,返回的一定不是null,从而在11行return退出,将获取到的task交给某个worker线程去执行。
workQueue中的元素从哪来呢 ?这就要回到最初的execute()方法了。
execute()
public class ThreadPoolExecutor extends AbstractExecutorService {
...
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
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);
}
...
}
前面以8行的参数为例,此时,线程池中的线程数未达到最小线程池的大小corePoolSize,通常这个时候直接在第10行返回。
如果不满足8行的条件,则会执行到13行的。isRunning(c)判断线程池是否未关闭,这里我们只关注未关闭的情况;接下来就会执行布尔表达式的第二部分workQueue.offer(command),尝试将任务放入到队列workQueue中。
workQueue.offer()的行为取决于线程池持有的BlockingQueue实例。
Executors.newFixedThreadPool()、Executors.newSingleThreadExecutor()创建的线程池使用LinkedBlockingQueue,而Executors.newCachedThreadPool()创建的线程池则使用SynchronousQueue。
public class Executors {
...
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
}
以LinkedBlockingQueue为例,创建时不配置容量,即创建为无界队列,则LinkedBlockingQueue#offer()永远返回true,从而进入execute()13-19行。
当workQueue.offer()返回true时,已经将任务command放入队列workQueue中。当未来某个时刻,某worker完成一个任务后,会通过getTask()从workerQueue在获取一个任务继续执行,直到线程池关闭,直到海枯石烂。
case2小结
可以看到,实际上,线程池的核心原理与对象池模型无关,而是生产者-消费者模型。
- 生产者(调用submit()或者execute()方法)将任务Task放入队列
- 消费者(worker线程)循环从任务队列中取出任务处理(执行task.run())
钩子方法
回到runWorker()方法,在执行任务的过程中,线程池保留了一些钩子方法,如beforeExecute()、afterExecute()。用户可以在实现自己的线程池时,可以通过覆写钩子方法为线程池添加功能。
总结
对象池和线程池关系不大,但是经常会混淆。线程池的实现很有意思。在追溯源码之前,我一直以为线程池就是把线程存起来,用的时候取出一个执行任务;看了源码才知道它实现如此之妙,简洁优雅效率高。源码才是最好的老师。