线程池的前世今生

对于一个话题,他不是读一本书,而是读五本。 -- Mike Slade To Bill Gates

昂贵的资源

我们都知道线程是一种昂贵的计算机资源。与普通的对象相比,线程占用了额外的栈空间,并且在启动与销毁的时候也会产生调度开销。另一方面来讲,我们设置线程的数量常常是要考虑处理器的数量,线程执行的任务特性等诸多方面。单以线程执行任务特性来讲,根据Amdahl‘s定律。我们能最优化设置线程数量的规则如下:

  • CPU密集型:这类任务主要消耗的是处理器的资源,为了避免处理器资源的浪费,我们设置线程数量为 CPU数量+1
  • I/O密集型:这种情况下,能用1个线程完成任务是最恰当的,因为多线程会引发额外的系统开销。当一个线程不能胜任的时候,因为等待I/O返回结果的时候是不占用处理器资源的,所以我们最理想的方式是 CPU数量×2

使用线程池是一种合理应用线程的方式去优化资源。

线程池的原理

线程池

与数据库连接池等对象池不同的是,线程池本身作为一个对象,它在内部创建好了一批线程,等待任务对象提交给线程池去执行。我们可以把线程池比作一个公司,这一批线程就是它的员工,有新的订单过来就会暂时放在一个箱子里(Job Queue),当有的员工空闲的时候就会排队去箱子里拿订单去按照要求生产。如上图所示,线程池本质就是一个生产者消费者模型,客户端相当于生产者,线程池内部的缓存队列相当于传输通道,线程池中的线程相当于消费者。

在Java的世界中,java.util.concurrent.ThreadPoolExecutor类就是一个线程池,我们可以调用submit()方法提交任务。在Java中,能表示一个线程的只有Thread类,除此之外的任何类都不能表示线程,Callable以及Runnable表示的是一个Task。以下给出了线程池的sumit方法描述。它的含义就是向线程池提交任务。

 /**
     * Submits a value-returning task for execution and returns a
     * Future representing the pending results of the task. The
     * Future's {@code get} method will return the task's result upon
     * successful completion.
     *
     * <p>
     * If you would like to immediately block waiting
     * for a task, you can use constructions of the form
     * {@code result = exec.submit(aCallable).get();}
     *
     * <p>Note: The {@link Executors} class includes a set of methods
     * that can convert some other common closure-like objects,
     * for example, {@link java.security.PrivilegedAction} to
     * {@link Callable} form so they can be submitted.
     *
     * @param task the task to submit
     * @param <T> the type of the task's result
     * @return a Future representing pending completion of the task
     * @throws RejectedExecutionException if the task cannot be
     *         scheduled for execution
     * @throws NullPointerException if the task is null
     */
    <T> Future<T> submit(Callable<T> task);

线程池中的参数

我们从之下的构造器中可以清楚地看到如果你想手动定义一个线程池都需要配备哪些参数。

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
  • corePoolSize核心线程数怎么理解呢?接着之上的比喻来说,核心线程数就相当于一个公司的干部储备,这批人自公司建立起就一直呆在公司了。也就是说核心线程数是自线程池接收到任务之前就已经初始化好了的。当然,在创建线程池的时候,核心线程并不会被创建,而是等待任务都提交之后才会启动创建,除非先前调用了prestartAllCoreThread
  • maximumPoolSize 最大线程数指的是线程池允许的最大线程的数量。
  • keepAliveTime 存活时间,指的是除了核心线程之外的线程在没有任务执行的时候,所能存活的时间,超过这个时间的线程就会被停止掉。
  • unit 时间单元
  • workQueue 指的是任务存放的队列,当任务数量过多超过了最大线程数,此时的任务会暂存在工作队列,等待线程执行完再来队列中拿任务。
  • threadFactory 线程工厂,生产线程的工厂,当任务超过核心线程数,少于最大线程数,线程的创建工作由线程工厂来完成。
  • handler 当任务数量过多,以至于工作队列存放满了,之后再被submit的任务会被拒绝接受,并抛出一个异常。但是注意,由submit提交被拒绝的任务不会中断线程池。由execute提交的任务再不做特殊处理的情况下,如果任务被拒绝,会中断线程池。

说完了使用构造器来创建线程池之后,我们可以看看使用Executors类中的工具包能创建的线程池,我觉得大多数初学者为了方便都会去使用一下的方法去创建线程池。

Executors中的线程池创建方法

拿其中一个方法来举例

    /**
     * Creates a thread pool that creates new threads as needed, but
     * will reuse previously constructed threads when they are
     * available.  These pools will typically improve the performance
     * of programs that execute many short-lived asynchronous tasks.
     * Calls to {@code execute} will reuse previously constructed
     * threads if available. If no existing thread is available, a new
     * thread will be created and added to the pool. Threads that have
     * not been used for sixty seconds are terminated and removed from
     * the cache. Thus, a pool that remains idle for long enough will
     * not consume any resources. Note that pools with similar
     * properties but different details (for example, timeout parameters)
     * may be created using {@link ThreadPoolExecutor} constructors.
     *
     * @return the newly created thread pool
     */
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

你会不幸的发现,他们并没有什么魔法,实现方式还是创建了ThreadPoolExecutor对象,你可能会想,这样创建线程不是挺好的么?封装了方法,很好的展示了面向对象的思想。但事实并非如此。

如果你安装了阿里巴巴的p3c插件你会注意到一件事情。

创建线程池

他会提醒你手动去创建一个线程池会更好,为什么这么说呢?他用以下例证来讲明了自己的观点。
为什么需要手动创建线程池

图片的文字可能有点小,总之,在多线程这个反直觉的编程模式下,我们的一切行为都要尽可能的保证可控,不带应用场景的滥用线程池设计就是在耍流氓...我们可以依照本文开头所提交到的规则去创建线程池。

线程池的饱和处理

在《Java并发编程实战》中,这里说的异常处理被成为饱和策略,在PoolThreadExecutor中有如下子类,他们表示被拒绝任务的处理方式。

实现类 饱和策略
AbortPolicy 直接抛出异常
DiscardOldestPolicy 丢弃workQueue中的旧任务,接纳新任务
CallerRunsPolicy 在客户端线程中执行被丢弃的任务
DiscardPolicy 丢弃被拒绝的任务

除此之外,我们也可以使用以下setRejectedExecutionHandler方法去设置饱和策略。

     RejectedExecutionHandler rejectedExecutionHandler = (r, executor) -> 
                executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

JDK中默认的Handler为AbortPolicy

   /**
     * The default rejected execution handler
     */
    private static final RejectedExecutionHandler defaultHandler =
        new AbortPolicy();

线程池的监控

ThreadPoolExecutor类中,还支持很多用于线程池监控的方法。

监控方法

我们可以调用这些方法,来确保线程池时刻处于健康的状态。此外线程池还支持void beforeExecute(Thread t, Runnable r)void afterExecute(Runnable r, Throwable t)两个钩子方法。也可以用作监控。

总结

还有一些线程池的问题,比方说,关于线程池死锁我们应该知道,同一个线程池只能执行相互独立的任务,有依赖的任务需要不同的线程池去执行
另一方面,从小习惯角度上来讲,在对线程池异常进行处理的时候,最好能捕获所有异常,并且包装一个RuningTimeException抛出出来。
相对于并发执行的任务,线程池为我们提供了一种优化且方便的执行手段。我们应当明白,框架工具再强大,也不能替我们完成所有事情,根据特定的任务制定特定的策略才是正确的打开方式。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,874评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,102评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,676评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,911评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,937评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,935评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,860评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,660评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,113评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,363评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,506评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,238评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,861评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,486评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,674评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,513评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,426评论 2 352

推荐阅读更多精彩内容