对象池和线程池

来源:互联网 发布:中华人软件下载 编辑:程序博客网 时间:2024/06/05 19:33

实际开发中,并发和多线程的情景处处存在。今天我们来聊聊对象池和线程池这个两个日常开发用的比较多,但是又很容易混淆的两个概念。

对象池

首先我们来说说对象池。对象池利用了串行封闭的概念:将对象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()。用户可以在实现自己的线程池时,可以通过覆写钩子方法为线程池添加功能。

总结

对象池和线程池关系不大,但是经常会混淆。线程池的实现很有意思。在追溯源码之前,我一直以为线程池就是把线程存起来,用的时候取出一个执行任务;看了源码才知道它实现如此之妙,简洁优雅效率高。源码才是最好的老师。