在MyThreadPool的内部,我们维护了一个阻塞队列workQueue和一组工作线程,工作线程的个数由构造函数中的poolSize来指定。用户通过调用execute()方法来提交Runnable任务,execute()方法的内部实现仅仅是将任务加入到workQueue中。MyThreadPool内部维护的工作线程会消费workQueue中的任务并执行任务,相关的代码就是代码①处的while循环。线程池主要的工作原理就这些,是不是还挺简单的?
Java并发包里提供的线程池,远比我们上面的示例代码强大得多,当然也复杂得多。Java提供的线程池相关的工具类中,最核心的是 ThreadPoolExecutor,通过名字你也能看出来,它强调的是Executor,而不是一般意义上的池化资源。
- ThreadPoolExecutor(
- int corePoolSize,
- int maximumPoolSize,
- long keepAliveTime,
- TimeUnit unit,
- BlockingQueue<Runnable> workQueue,
- ThreadFactory threadFactory,
- RejectedExecutionHandler handler)
下面我们逐一介绍这些参数的含义,将线程池类比为一个项目组,而每个线程就是项目组的成员。
- Z4 [+ m5 G0 W/ k: H5 Z; n \$ w- corePoolSize:表示线程池所持有的最小线程数。有些项目可能很闲,但也不能将所有项目组成员都撤离,至少要留下corePoolSize个人守在岗位上。
- maximumPoolSize:表示线程池可创建的最大线程数。当项目繁忙时,需要增加成员,但也不能无限制地增加,最多增加到maximumPoolSize个人。当项目变得闲暇时,需要减少成员,最多将成员减至corePoolSize个人。
- keepAliveTime & unit:前面提到,项目根据忙闲来增减成员。在编程世界中,如何定义忙和闲呢?很简单,如果一个线程在一段时间内都没有执行任务,说明它处于闲置状态。而keepAliveTime和unit就用来定义这段时间的参数。也就是说,如果一个线程空闲了keepAliveTime & unit这么长时间,并且线程池的线程数超过了corePoolSize,那么该空闲线程就会被回收。
- workQueue:工作队列,与上面示例代码中的工作队列意义相同。
- threadFactory:通过该参数,你可以自定义如何创建线程。例如,你可以为线程指定一个有意义的名称。
- handler:通过该参数,你可以自定义任务的拒绝策略。如果线程池中所有的线程都在忙碌,且工作队列已满(前提是工作队列是有界队列),此时提交任务,线程池将会拒绝接收。至于拒绝策略,你可以通过handler参数来指定。ThreadPoolExecutor已经提供了以下四种策略:
- CallerRunsPolicy:提交任务的线程自行执行该任务。
- AbortPolicy:默认的拒绝策略,会抛出RejectedExecutionException异常。
- DiscardPolicy:直接丢弃任务,没有任何异常抛出。
- DiscardOldestPolicy:丢弃最老的任务,实际上是丢弃最早进入工作队列的任务,并将新任务加入工作队列。3 J; l" |4 r4 V. U7 W+ o
Java 1.6版本还增加了allowCoreThreadTimeOut(boolean value)方法,它可以使所有线程都支持超时。这意味着如果项目很闲,项目组的成员都会被撤离。
- F3 Q& g! u! T& Y" H- J使用线程池要注意些什么: w. ?# }6 q& F! D5 F! H2 _) P
考虑到ThreadPoolExecutor的构造函数相对复杂,Java并发包提供了一个线程池的静态工厂类Executors,通过Executors可以快速创建线程池。不过,目前大型公司的编码规范一般不建议使用Executors,所以我就不再详细介绍这方面内容了。
8 R: d. @- b+ ^
不建议使用Executors最重要的原因是:Executors提供的许多方法默认使用**的LinkedBlockingQueue。在高负载情况下,**队列很容易导致OOM(内存溢出),而OOM会导致所有请求都无法处理,这是一个严重的问题。因此,强烈建议使用有界队列。
E* ~% s! _' r ^/ z# J1 e使用有界队列时,当任务过多时,线程池会触发执行拒绝策略。线程池的默认拒绝策略会抛出RejectedExecutionException异常,这是一个运行时异常,编译器不会强制要求捕获它,因此开发人员很容易忽略。因此,在使用默认拒绝策略时要谨慎。如果线程池处理的任务非常重要,建议自定义拒绝策略,并在实际工作中将自定义的拒绝策略与降级策略配合使用。
3 F+ T$ s% j8 B8 T在使用线程池时,还需要注意异常处理的问题。例如,通过ThreadPoolExecutor对象的execute()方法提交任务时,如果任务在执行过程中出现运行时异常,会导致执行该任务的线程终止。然而,最致命的是尽管任务发生异常,你却无法获得任何通知,这可能让你误以为任务都正常执行了。尽管线程池提供了许多用于异常处理的方法,但最可靠和简单的方案是捕获所有异常并根据需要进行处理,你可以参考下面的示例代码。
9 B# H5 J5 [9 M( _- try {
- //业务逻辑
- } catch (RuntimeException x) {
- //按需处理
- } catch (Throwable x) {
- //按需处理
- }
总结
0 y0 l. j6 M# Y! Y2 ~5 h线程池在Java并发编程领域中扮演着重要角色,许多大型公司的编码规范要求使用线程池来管理线程。线程池与普通的资源池有很大的区别,实际上它是生产者-消费者模式的一种实现。理解生产者-消费者模式是理解线程池的关键所在。