在Java语言中,创建线程并不像创建对象一样简单。虽然只需要使用new Thread()即可创建线程,但实际上创建线程比创建对象复杂得多。创建对象只需在JVM的堆中分配内存,而创建线程需要调用操作系统内核的API,并为线程分配一系列资源,这个成本相对较高。因此,线程被视为重量级的对象,应尽量避免频繁创建和销毁。" F H0 u. s! g0 ^+ y
! E' }( U' w: K x; I4 A
那么如何避免频繁创建线程呢?解决方案就是使用线程池。$ |9 n% ?2 ^1 B1 T
由于线程池的需求非常普遍,所以Java SDK的并发包自然也包含了线程池。但是,很多人初次接触并发包中与线程池相关的工具类时,可能会感到有些困惑,不知从何入手。我认为,这主要是因为线程池与通常意义上的资源池是不同的。一般意义上的资源池在需要资源时调用acquire()方法申请资源,在使用完毕后调用release()释放资源。然而,如果你带着这种固有模型来看待并发包中的线程池相关工具类,会遗憾地发现它们与之不匹配,因为Java提供的线程池中根本不存在申请线程和释放线程的方法。 ! Z' ~# l/ S0 \
ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
下面我们逐一介绍这些参数的含义,将线程池类比为一个项目组,而每个线程就是项目组的成员。. D! _- O; e1 t U# f
DiscardOldestPolicy:丢弃最老的任务,实际上是丢弃最早进入工作队列的任务,并将新任务加入工作队列。- N; E. F) d$ X9 a" [
Java 1.6版本还增加了allowCoreThreadTimeOut(boolean value)方法,它可以使所有线程都支持超时。这意味着如果项目很闲,项目组的成员都会被撤离。 4 Q# v( W F. L' T F使用线程池要注意些什么: s4 A E' u+ V
考虑到ThreadPoolExecutor的构造函数相对复杂,Java并发包提供了一个线程池的静态工厂类Executors,通过Executors可以快速创建线程池。不过,目前大型公司的编码规范一般不建议使用Executors,所以我就不再详细介绍这方面内容了。 1 _# t3 E( A4 t/ e h( V+ Z不建议使用Executors最重要的原因是:Executors提供的许多方法默认使用**的LinkedBlockingQueue。在高负载情况下,**队列很容易导致OOM(内存溢出),而OOM会导致所有请求都无法处理,这是一个严重的问题。因此,强烈建议使用有界队列。6 E/ \' r" q. N9 h c
使用有界队列时,当任务过多时,线程池会触发执行拒绝策略。线程池的默认拒绝策略会抛出RejectedExecutionException异常,这是一个运行时异常,编译器不会强制要求捕获它,因此开发人员很容易忽略。因此,在使用默认拒绝策略时要谨慎。如果线程池处理的任务非常重要,建议自定义拒绝策略,并在实际工作中将自定义的拒绝策略与降级策略配合使用。 : b5 I V0 ]8 b. G在使用线程池时,还需要注意异常处理的问题。例如,通过ThreadPoolExecutor对象的execute()方法提交任务时,如果任务在执行过程中出现运行时异常,会导致执行该任务的线程终止。然而,最致命的是尽管任务发生异常,你却无法获得任何通知,这可能让你误以为任务都正常执行了。尽管线程池提供了许多用于异常处理的方法,但最可靠和简单的方案是捕获所有异常并根据需要进行处理,你可以参考下面的示例代码。 & D# o& ]$ f/ `! @0 O/ _4 j