Java线程池的原理及几类线程池的介绍

发布时间:2023-05-09 10:01:07

  在什么情况下使用线程池?处理单个任务的时间相对较短 需要处理的任务数量很大

  使用线程池的好处: 减少创建和销毁线程的时间和系统资源的成本 如果不使用线程池,系统可能会创建大量的线程,导致系统内存的消耗和“过度切换”。

  线程池工作原理:

  为什么要使用线程池?

  诸如 Web 许多服务器应用程序,如服务器、数据库服务器、文件服务器或电子邮件服务器,都来自一些远程服务器来源大量的短任务。请求以某种方式到达服务器,这可能是通过网络协议(例如 HTTP、FTP 或 POP)、通过 JMS 队列或可能通过轮询数据库。无论请求如何到达,服务器应用程序中经常出现的情况是,单个任务处理时间很短,但请求数量巨大。

  构建服务器应用程序的一个过于简单的模型应该是,每当一个请求到达时,创建一个新的线程,然后在新的线程中为请求服务。事实上,原型开发工作得很好,但如果试图部署以这种方式运行的服务器应用程序,这种方法的严重缺陷是显而易见的。每个请求对应一个线程(thread-per-request)方法的缺点之一是,为每个请求创建一个新线程的成本非常高;为每个请求创建新线程的服务器在创建和销毁线程上花费的时间和系统资源比处理实际用户请求的时间和资源要多。

  除了创建和销毁线程的费用外,活动线程还消耗系统资源。在一个 JVM 创建过多的线程可能会导致系统过度消耗内存或“过度切换”。为了防止资源不足,服务器应用程序需要一些方法来限制任何给定时间处理的请求数量。

  线程池为线程生命周期费用和资源不足提供了解决方案。线程创建的费用通过重用多个任务的线程分配到多个任务上。其优点是,由于线程已经存在于请求到达时,线程创建带来的延迟也被无意中消除。这样,应用程序就可以立即响应请求服务更快。此外,通过适当调整线程池中的线程数量,即当要求数量超过一定阈值时,迫使任何其他新请求等待,直到获得线程处理,以防止资源不足。

  替代线程池

  线程池远不是服务器应用程序中使用多线程的唯一方法。正如上面提到的,有时为每个新任务生成一个新的线程是明智的。然而,如果任务创建过于频繁,任务的平均处理时间过短,则为每个任务生成一个新的线程将导致性能问题。

  另一种常见的线程模型是为某一类型的任务分配后台线程和任务队列。AWT 和 Swing 使用这个模型,这个模型中有一个 GUI 所有导致用户界面变化的工作都必须在这个线程中执行。但是,因为只有一个 AWT 因此,线程必须在 AWT 在线程中执行任务可能需要很长时间,这是不可取的。因此,Swing 应用程序通常需要额外的工作线程,用于运行时间长的工作线程 UI 相关任务。

  每个任务对应一个线程方法和一个后台线程(single-background-thread)在某些情况下,方法工作非常理想。在只有少量运行时间长的任务时,每个任务的一个线程方法工作得很好。只要调度的可预见性不是很重要,单个后台线程的方法就会做得很好,比如低优先级后台任务。然而,大多数服务器应用程序都是为了处理大量的短期任务或子任务,所以他们通常希望有一种机制,以低成本有效地处理这些任务,以及一些资源管理和定期可预测的措施。线程池提供了这些优点。

  工作队列

  就线程池的实际实现而言,术语“线程池”有些误解,因为在大多数情况下,线程池“明显”的实现并不一定会产生我们想要的结果。术语“线程池”先于 Java 这个平台出现了,所以它可能是面向对象方法较少的产物。然而,这个术语仍然被广泛使用。

  虽然我们可以很容易地实现一个线程池,包括客户机器等待一个可用的线程,将任务传输到线程执行,然后在任务完成时将线程返回到池,但这种方法有几个潜在的负面影响。比如池子空的时候会发生什么?试图将任务传递到池线程的调用器会发现池是空的,当调用器等待可用的池线程时,其线程会被堵塞。使用后台线程的原因通常是为了防止正在提交的线程被堵塞。如果在线程池中实现“明显”的情况,完全堵塞调用器,可以防止我们试图解决的问题。

  我们通常想要的是结合同一组固定工作线程的工作队列,它使用 wait() 和 notify() 通知等待线程的新工作已经到了。该工作队列通常被实现为具有相关监视器对象的链表。清单 1 简单的合用工作队列示例显示。尽管 Thread API 没有对使用 Runnable 界面强加特殊要求,但使用时使用 Runnable 这种对象队列模式是调度程序和工作队列的公共协议。

  清单 1. 有线程池的工作队列 1. public class WorkQueue 2. { 3. private final int nThreads; 4. private final PoolWorker[] threads; 5. private final LinkedList queue; 6. public WorkQueue(int nThreads) 7. { 8. this.nThreads = nThreads; 9. queue = new LinkedList(); 10. threads = new PoolWorker[nThreads]; 11. for (int i=0; i

  你可能已经注意到清单了 1 在实现中使用的是 notify() 而不是 notifyAll() 。大多数专家建议使用 notifyAll() 而不是 notify() ,而且理由很充分:使用: notify() 只有在某些特定条件下使用这种方法,才有难以捉摸的风险。另一方面,如果使用得当, notify() 具有比 notifyAll() 更可取的性能特征;特别是, notify() 服务器应用程序中引起的环境切换要少得多。

  清单 1 示例工作队列满足安全使用要求 notify() 需求。因此,请继续在您的程序中使用,但在其他情况下使用 notify() 请格外小心。

  使用线程池的风险

  虽然线程池是构建多线程应用程序的强大机制,但使用它并非没有风险。用线程池构建的应用程序容易遭受其他多线程应用程序容易遭受的所有并发风险,如同步错误和死锁,也容易遭受少数其他特定于线程池的风险,如死锁、资源不足和线程泄漏。

  死锁

  任何多线程应用程序都有死锁的风险。当一组过程或线程中的每个人都在等待一个只能由另一组过程引起的事件时,我们将讨论这组过程或线程 死锁了。死锁最简单的情况是:线程 A 持有对象 X 独家锁,并在等待对象 Y 锁,线程 B 持有对象 Y 独家锁正在等待对象 X 的锁。除非有办法打破锁的等待(Java 锁定不支持这种方法),否则死锁的线程将永远等待。

  虽然任何多线程序都有死锁的风险,但线程池引入了另一种死锁的可能性。在这种情况下,所有的池线程都在执行阻塞的任务,等待队列中另一个任务的执行结果,但这项任务无法运行,因为没有未被占用的线程。当线程池被用来模拟许多交互对象时,模拟对象可以相互发送查询。这些查询将作为排队任务执行,当查询对象同步等待响应时。

  资源不足

  线程池的一个优点是,与其他替代调度机制(我们讨论过一些)相比,它们通常执行得很好。但只有当线程池的大小得到适当调整时。线程消耗了大量的资源,包括内存和其他系统资源。除了 Thread 除了对象所需的内存,每个线程都需要两个可能很大程度上执行调用堆栈。此外,JVM 每一个都有可能 Java 创建一个线程将消耗额外的系统资源。最后,虽然线程之间切换的调度成本很小,但如果线程较多,环境切换也可能严重影响程序的性能。

  如果线程池太大,线程消耗的资源可能会严重影响系统性能。在线程之间切换会浪费时间,使用比你实际需要的更多的线程可能会导致资源短缺,因为池线程消耗一些资源,其他任务可能会更有效地利用它们。除了线程本身使用的资源外,服务请求可能需要其他资源,例如 JDBC 连接、套接字或文件。这些也是有限的资源,太多的并发请求也可能导致失败,如无法分配 JDBC 连接。

  并发错误

  依靠使用线程池和其他排队机制 wait() 和 notify() 这两种方法都很难使用。如果编码不正确,可能会丢失通知,导致线程保持空闲,尽管队列中有工作要处理。使用这些方法时,一定要格外小心;即使是专家也可能在上面犯错误。最好使用现有的,已经知道可以工作的实现,比如下面的 不需要在自己的池子里写讨论的东西 util.concurrent 包。

  线程泄漏

  各种类型的线程池的严重风险之一是线程泄漏。这种情况发生在从池中删除一个线程以执行任务,但在任务完成后没有返回到池中。线程泄漏的情况出现在任务中抛出一个 RuntimeException 或一个 Error 时。如果池类没有捕捉到它们,线程只会退出,线程池的大小会永久减少一个。当这种情况发生得足够多时,线程池最终是空的,系统将停止,因为没有可用的线程来处理任务。

  有些任务可能总是等待某些资源或用户的输入,这些资源不能保证可用,用户可能已经回家,这些任务将永久停止,这些任务也会导致与线程泄漏相同的问题。如果一个线程被这样的任务永久消耗,那么它实际上就被从池中删除了。对于这样的任务,要么只给他们自己的线程,要么只让他们等有限的时间。

  请求过载

  有可能仅仅通过请求就压垮服务器。在这种情况下,我们可能不想把每一个到来的请求都排在我们的工作队列上,因为等待在队列中执行的任务可能会消耗太多的系统资源,导致资源不足。在这种情况下,决定如何做取决于你自己;在某些情况下,你可以简单地放弃请求,依靠更高水平的协议,你也可以用一个指出服务器暂时忙碌的响应来拒绝请求。

  有效利用线程池的标准

  只要您遵循几个简单的标准,线程池就可以成为构建服务器应用程序的一种极其有效的方法:

  不要对同步等待其他任务结果的任务排队。这可能会导致上述形式的死锁。在那种死锁中,所有的线程都被一些任务占据。这些任务依次等待排队任务的结果,这些任务无法执行,因为所有的线程都很忙。

  使用合用线程时要小心,可能需要很长时间。如果程序必须等待,例如 I/O 如果您完成此类资源,请指定最长的等待时间,以及是故障还是将任务重新排队,以便以后执行。这样做保证了将线程释放到可能成功完成的任务中 某些进展。

  理解任务。要有效地调整线程池的大小,你需要了解排队的任务以及他们在做什么。它们是 CPU 限制的(CPU-bound)吗?它们是 I/O 限制的(I/O-bound)吗?您的答案将影响您如何调整应用程序。如果你有不同的任务类别,这些类别有完全不同的特征,那么为不同的任务类别设置多个工作队列可能是有意义的,这样你就可以相应地调整每个池。

  调整池的大小

  调整线程池的大小基本上是为了避免两种错误:线程太少或线程太多。幸运的是,对于大多数应用程序来说,太多和太少之间的空间相当宽。

  请回忆:在应用程序中使用线程有两个主要优点,尽管等待 I/O 慢操作,但允许继续处理,可以使用多处理器。在运行中拥有 N 在处理器机器上的计算限制应用程序中,在线程数接近 N 添加额外的线程可能会提高总处理能力,而在线程数量超过 N 添加额外的线程将不起作用。事实上,太多的线程甚至会降低性能,因为它会导致额外的环境切换成本。

  线程池的最佳尺寸取决于可用处理器的数量和工作队列中任务的性质。如果有 N 处理器系统中只有一个工作队列,都是计算性质的任务,在线程池 N 或 N+1 一般来说,一个线程会得到最大的 CPU 利用率。

  那些可能需要等待的人可能需要等待 I/O 完成的任务(例如,从套接字读取 HTTP 要求的任务)需要使池的大小超过可用处理器的数量,因为并不是所有的线程都在工作。通过使用概要分析,您可以估计典型请求的等待时间(WT)与服务时间(ST)两者之间的比例。如果我们称之为这个比例,如果我们称之为这个比例 WT/ST,所以对于一个拥有 N 需要设置一个处理器系统 N*(1+WT/ST) 保持处理器充分利用一个线程。

  处理器利用率并不是调整线程池大小的唯一考虑因素。随着线程池的增长,您可能会遇到调度程序、可用内存或其他系统资源的限制,如连接字、打开的文件句柄或数据库连接。

  不需要写自己的池

  Doug Lea 编写了一个优秀的并发实用程序开放源码库 util.concurrent ,它包括相互排斥、信号量,如并发访问下执行良好的队列和散列表,以及实现几个工作队列。该包中的 PooledExecutor 以工作队列为基础的线程池的正确实现,是一种有效、广泛使用的线程池。你不必试图写自己的线程池,这很容易出错。相反,你可以考虑使用它 util.concurrent 一些实用程序。参阅 获取链接和更多信息的参考资料。

  util.concurrent 库也激发了 JSR 166,JSR 166 是一个 Java 社区过程(Java Community Process (JCP))他们正计划开发一个包括在内的工作组 java.util.concurrent 包下的 Java 并发实用程序在类库中,这个包应该用来 Java 开发工具箱 1.5 发行版。

  线程池是组织服务器应用程序的有用工具。概念上很简单,但是在实现和使用一个池的时候,需要注意几个问题,比如死锁、资源不足和 wait() 及 notify() 的复杂性。如果您发现您的应用程序需要线程池,请考虑使用它 util.concurrent 中的某个 Executor 类,例如 PooledExecutor ,而不是从头开始写作。如果要创建自己的线程来处理生存期短的任务,一定要考虑用线程池代替。

  该文章有一个简单描述线程池内部实现的例子。建议根据其中的例子了解JAVA 线程池的原理。同时,它还详细描述了使用线程池的优缺点。你可以研究一下。我认为这是一篇很好的文章。

  JDK自带线程池总类介绍:

  1、newfixedthreadpol创建了指定工作线程数量的线程池。每当提交任务时,创建一个工作线程。如果工作线程的数量达到最初的最大数量,则将提交的任务存储在池队列中。

  2、newcachedthreadPol创建了一个缓存线程池。这种类型的线程池具有以下特点:

  1).创建工作线程的数量几乎没有限制(实际上也有限制,数量是Interger. MAX_VALUE), 线程可以灵活地添加到线程池中。

  2).如果长时间未向线程池提交任务,即如果工作线程有空(默认为1分钟),工作线程将自动终止。终止后,如果您提交了新的任务,则线程池将重新创建一个工作线程。

  3、newsinglethreadexecutor创建一个单线程Executor,即只创建一个工作人员线程来执行任务。如果线程异常结束,将有另一个替换,以确保顺序执行(我认为这是它的特点)。单工作线程最大的特点是确保每个任务的顺序执行,并且在任何给定的时间内都不会有多个线程是活动的 。

  4、newschedulethreadPol创建了一个长线程池,支持定期和周期性的任务执行,类似于timer。(这个线程池的原理还没有完全理解)

  总结:

  一.FixedThreadPol是一个典型而优秀的线程池,具有提高程序效率、节省线程创建成本的优点。然而,当在线程池是免费的,即当线程池中没有可操作的任务时,它不会释放工作线程,并占用一定的系统资源。

  二.CachedThreadPool的特点是,当在线程池空闲时,即当线程池中没有可操作任务时,它会释放工作线程,从而释放工作线程占用的资源。但是,当出现新任务时,需要创建新的工作线程和一定的系统费用。而且,在使用CachedThreadPol时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很可能导致系统瘫痪。

上一篇 array和list的区别
下一篇 常见的并发场景

文章素材均来源于网络,如有侵权,请联系管理员删除。

标签: Java教程Java基础Java编程技巧面试题Java面试题