尽管线程池对于构建多线程应用是个很强大的机制,但它也不是没有缺点的。使用线程池构建的应用会面临其他多线程应用所面对的一样的并发风险,比如同步错误和死锁,此外线程池还有其他的一些特有缺陷,比如 线程池-关联 死锁,资源不足,还有线程泄漏。
死锁
任何多线程应用都会面临死锁的风险。彼此双方都在等待一个事件,而这个事件只能有对方提供,这样一对进程或者线程我们称之为死锁。死锁最简单的情况是线程 A 持有了对象 X 的独占锁,线程 A 在等待对象 Y 的锁,而线程 B 恰恰持有了对象 Y 的独占锁,线程 B 在等待对象 X 的锁。除非有某种办法能够打破这种锁等待(Java 锁机制不能支持这个),否则的话这一对死锁线程将会永久地等待下去。既然死锁是所有多线程编程都将面临的风险,线程池为我们引入了另一种死锁:线程池中所有线程都在阻塞等待队列中另一个任务的执行结果,但是另一个任务无法得到执行,因为池中压根儿就没用空闲的可用线程。这种情况在线程池用于一些相互影响对象的模拟实现中可能会出现,这些模拟对象彼此发送查询然后作为任务队列进行执行,发起查询的对象同步等待响应。
资源不足
线程池的优点之一是他们在大多数情况下比其他的调度机制具备更好的性能,比如我们上面所讨论的那几种。但这个取决于你有没有恰当地配置了线程池大小。线程占用大量的资源,包括内存和其他系统资源。除了线程对象所必须的内存之外,每个线程还需要两个执行调用栈,这个栈可能会很大。此外,JVM 可能还会为每个 Java 线程创建一个本地线程,这样将会占用额外的系统资源。最后,虽然线程之间切换的调度开销很小,大量的线程上下文切换也会影响到你的应用性能。如果线程池过大的话,这些众多线程所消耗的资源将会明显影响到系统性能。时间会浪费在线程之间的切换上,配置有比你实际需要更多的线程会引起资源不足的问题,因为池中线程所占用的资源如果用在其他任务上可能会更高效。除了这些线程本身所使用的资源之外,服务请求时所做的工作可能会需要额外资源,比如 JDBC 连接,套接字,或者文件。这些也是有限的资源,而且对它们进行过高并发请求的话可能会导致失效,比如无法分配一个 JDBC 连接。
并发错误
线程池以及其他队列机制依赖于 wait() 和 notify() 方法的使用,这可能会变得很棘手。如果编码不当的话,很可能会导致通知丢失,结果就是池中的线程都处于一个空闲的状态,而实际上队列中有任务需要处理。在使用这些工具的时候要打起十二万分的精神;即便是专家在用它们的时候也经常会失误。幸运的是,可以使用一些现成的实现,这些实现久经考验,比如下文将会讨论到的 你无须自行编码 实现的 java.util.concurrent 包。线程泄漏
各种各样的线程池中存在的一个重大的危险就是线程泄漏,当一个线程被从线程池中移除去执行一个任务,任务执行结束之后却没有返还给线程池的时候,就会出现这种危险。出现这种情况的一种方式是当任务抛出一个 RuntimeException 或一个 Error 时。如果线程池类没有捕捉到这些,该线程将会傻傻地存在于线程池之中,而线程池的线程数量则会被永久地减一。当这种情况发生的次数足够多的时候,线程池最终将为空(无可用线程),而系统则会瘫痪,因为已经没有线程来处理任务了。瘫痪的任务,比如那些永久等待不保证可用资源或者等待已经回家了的用户输入的任务,也可以造成相等于线程泄漏一样的后果。如果一个线程永久地被这样一个任务所占用了的话,它造成的影响和从池中移除是一样的。像这样的任务应该要么给它们一个线程池之外的线程,要么控制一下它们的等待时间。