专业的建设机械网站制作wordpress主题更新了
web/
2025/10/7 0:01:37/
文章来源:
专业的建设机械网站制作,wordpress主题更新了,中国大型网站建设公司,广告代理动态调整线程池无论您是否知道#xff0c;您的Java Web应用程序很可能都使用线程池来处理传入的请求。 这是许多人忽略的实现细节#xff0c;但是迟早您需要了解如何使用该池以及如何为您的应用程序正确调整池。 本文旨在说明线程模型#xff0c;线程池是什么以及正确配置线… 动态调整线程池 无论您是否知道您的Java Web应用程序很可能都使用线程池来处理传入的请求。 这是许多人忽略的实现细节但是迟早您需要了解如何使用该池以及如何为您的应用程序正确调整池。 本文旨在说明线程模型线程池是什么以及正确配置线程池所需执行的操作。 单螺纹 让我们从一些基础知识开始并随着线程模型的发展而前进。 无论您使用哪种应用程序服务器或框架 Tomcat Dropwizard Jetty 它们都使用相同的基本方法。 一个深埋在Web服务器内部的套接字。 该套接字正在侦听传入的TCP连接并接受它们。 一旦接受就可以从新建立的TCP连接中读取数据进行解析并将其转换为HTTP请求。 然后将此请求移交给Web应用程序以完成其所需的操作。 为了理解线程的作用我们将不使用应用程序服务器而是从头开始构建一个简单的服务器。 该服务器反映了大多数应用程序服务器的功能。 首先单线程Web服务器可能如下所示 ServerSocket listener new ServerSocket(8080);
try {while (true) {Socket socket listener.accept();try {handleRequest(socket);} catch (IOException e) {e.printStackTrace();}}
} finally {listener.close();
} 此代码在端口8080上创建一个ServerSocket 然后在紧密循环中ServerSocket检查要接受的新连接。 接受后套接字将传递给handleRequest方法。 该方法通常会读取HTTP请求执行所需的任何过程并编写响应。 在此简单示例中handleRequest读取一行并返回简短的HTTP响应。 handleRequest做一些更复杂的事情是正常的例如从数据库中读取或进行某种其他类型的IO。 final static String response “HTTP/1.0 200 OK\r\n” “Content-type: text/plain\r\n” “\r\n” “Hello World\r\n”;public static void handleRequest(Socket socket) throws IOException {// Read the input stream, and return “200 OK”try {BufferedReader in new BufferedReader(new InputStreamReader(socket.getInputStream()));log.info(in.readLine());OutputStream out socket.getOutputStream();out.write(response.getBytes(StandardCharsets.UTF_8));} finally {socket.close();}
} 由于只有一个线程处理所有接受的套接字因此在接受下一个请求之前必须完全处理每个请求。 在实际的应用程序中等效的handleRequest方法返回大约100毫秒是正常的。 如果是这种情况服务器将被限制为每秒仅处理10个请求一个接一个。 多线程 即使handleRequest可能在IO上被阻止CPU也可以自由处理更多请求。 使用单线程方法是不可能的。 因此可以通过创建多个线程来改进此服务器以允许并发操作 public static class HandleRequestRunnable implements Runnable {final Socket socket;public HandleRequestRunnable(Socket socket) {this.socket socket;}public void run() {try {handleRequest(socket);} catch (IOException e) {e.printStackTrace();}}
}ServerSocket listener new ServerSocket(8080);
try {while (true) {Socket socket listener.accept();new Thread(new HandleRequestRunnable(socket)).start();}
} finally {listener.close();
} 在这里仍然在单个线程内的紧密循环中调用accept但是一旦接受TCP连接并且有可用的套接字就会产生一个新线程。 这个产生的线程执行一个HandleRequestRunnable它从上面简单地调用相同的handleRequest方法。 创建新线程后现在可以释放原始的accept线程来处理更多的TCP连接并允许应用程序同时处理请求。 该技术被称为“每个请求线程”是最流行的方法。 值得注意的是还有其他方法例如事件驱动的异步模型NGINX和Node.js部署但是它们不使用线程池因此不在本文讨论范围之内。 在每个请求线程数方法中创建新线程然后销毁它可能会很昂贵因为JVM和OS都需要分配资源。 另外在上述实现中正在创建的线程数不受限制。 不受限制是很成问题的因为它会很快导致资源枯竭。 资源枯竭 每个线程都需要一定数量的内存用于堆栈。 在最新的64位JVM上 默认堆栈大小为1024KB。 如果服务器收到大量请求或者handleRequest方法变慢则服务器可能会出现大量并发线程。 因此要管理1000个并发请求仅1000个线程将消耗1GB的JVM RAM仅用于线程的堆栈。 另外在每个线程中执行的代码将在处理请求所需的堆上创建对象。 这很快就会加起来并且可能超过分配给JVM的堆空间从而对垃圾收集器施加压力导致崩溃并最终导致OutOfMemoryErrors 。 线程不仅消耗RAM而且可能使用其他有限资源例如文件句柄或数据库连接。 超过这些可能导致其他类型的错误或崩溃。 因此为了避免耗尽资源重要的是避免无限制的数据结构。 不是万能的但是可以通过使用-Xss标志调整堆栈大小来缓解堆栈大小问题。 较小的堆栈将减少每个线程的开销但可能导致StackOverflowErrors 。 您的里程会有所不同但是对于许多应用程序默认的1024KB过多而更小的256KB或512KB的值可能更合适。 Java允许的最小值是16KB。 线程池 为了避免连续创建新线程并限制最大数量可以使用一个简单的线程池。 简而言之该池跟踪所有线程在需要达到上限时创建新线程并在可能的情况下重用空闲线程。 ServerSocket listener new ServerSocket(8080);
ExecutorService executor Executors.newFixedThreadPool(4);
try {while (true) {Socket socket listener.accept();executor.submit( new HandleRequestRunnable(socket) );}
} finally {listener.close();
} 现在此代码不是直接创建线程而是使用ExecutorService它提交要在线程池中执行的工作用Runnables术语。 在此示例中四个线程的固定线程池用于处理所有传入的请求。 这限制了“进行中”请求的数量因此限制了资源的使用。 除了newFixedThreadPool之外 Executors实用程序类还提供了newCachedThreadPool方法。 这受到较早的无限线程数量的困扰但是只要有可能就利用先前创建但现在空闲的线程。 通常这种类型的池对于不阻塞外部资源的短暂请求很有用。 ThreadPoolExecutors可以直接构造从而可以自定义其行为。 例如可以定义池中线程的最小和最大数量以及何时创建和销毁线程的策略。 简短的例子。 工作队列 在固定线程池的情况下细心的读者可能想知道如果所有线程都忙并且有新的请求进入该怎么办。那么ThreadPoolExecutor使用队列来保存线程可用之前的待处理请求。 默认情况下Executors.newFixedThreadPool和Executors.newCachedThreadPool都使用无界LinkedList。 同样这会导致资源耗尽问题尽管速度要慢得多因为每个排队的请求都小于一个完整的线程并且通常不会使用那么多资源。 但是在我们的示例中每个排队的请求都持有一个套接字取决于操作系统将占用一个文件句柄。 这是操作系统将限制的资源因此除非有必要否则最好不要保留它。 因此限制工作队列的大小也很有意义。 public static ExecutorService newBoundedFixedThreadPool(int nThreads, int capacity) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueueRunnable(capacity),new ThreadPoolExecutor.DiscardPolicy());
}public static void boundedThreadPoolServerSocket() throws IOException {ServerSocket listener new ServerSocket(8080);ExecutorService executor newBoundedFixedThreadPool(4, 16);try {while (true) {Socket socket listener.accept();executor.submit( new HandleRequestRunnable(socket) );}} finally {listener.close();}
} 再次我们创建了一个线程池但是我们没有使用Executors.newFixedThreadPool帮助器方法而是自己创建了ThreadPoolExecutor并传递了一个限制为16个元素的有界LinkedBlockingQueue 。 或者可以使用ArrayBlockingQueue 它是有界缓冲区的实现。 如果所有线程都忙并且队列已满则下一步将由ThreadPoolExecutor的最后一个参数定义。 在此示例中使用了DiscardPolicy 它只是丢弃将使队列溢出的所有工作。 还有其他政策如AbortPolicy它抛出一个异常或CallerRunsPolicy执行该调用者的线程上的工作。 此CallerRunsPolicy提供了一种简单的方法来自我限制可以添加作业的速率但是这可能是有害的阻塞了应保持不受阻塞的线程。 一个好的默认策略是“放弃”或“中止”这两者都会放弃工作。 在这些情况下很容易将简单错误返回给客户端例如HTTP 503“服务不可用” 。 有人会争辩说只是增加队列大小然后所有工作最终都会运行。 但是用户不愿永远等待如果从根本上说工作进入的速度超过了可以执行的速度那么队列将无限期地增长。 相反该队列仅应用于消除突发请求或处理处理中的短暂停顿。 在正常操作中队列应为空。 有多少个线程 现在我们了解了如何创建线程池困难的问题是应该有多少个线程可用 我们确定最大数量应该限制为不导致资源耗尽。 这包括所有类型的资源内存堆栈和堆打开的文件句柄打开的TCP连接远程数据库可以处理的连接数以及任何其他有限资源。 相反如果线程是与CPU绑定而不是与IO绑定则应将物理核的数量视为有限并且每个核最多只能创建一个线程。 这一切都取决于应用程序正在执行的工作。 用户应使用各种池大小以及实际的请求混合来运行负载测试。 每次增加它们的线程池大小直到断点。 这样就可以在资源耗尽时找到上限。 在某些情况下明智的做法是增加可用资源的数量例如为JVM提供更多的RAM或者调整OS以允许更多的文件句柄。 但是在某个时候会达到理论上限应该注意但这还不是故事的结局。 利特尔定律 排队论尤其是利特尔定律 可以用来帮助理解线程池的属性。 简单来说利特尔定律描述了三个变量之间的关系。 L进行中的请求数λ新请求到达的速率W平均处理该请求的时间。 例如如果每秒有10个请求到达并且每个请求花费一秒钟的时间来处理则在任何时间平均有10个正在进行的请求。 在我们的示例中这映射为使用10个线程。 如果处理单个请求的时间增加了一倍则运行中的平均请求数也将增加一倍达到20因此需要20个线程。 了解执行时间对进行中的请求的影响非常重要。 某些后端资源例如数据库停顿是很常见的导致请求花费更长的时间来处理从而很快耗尽了线程池。 因此理论上限可能不是池大小的适当限制。 相反应该对执行时间设置一个限制并与理论上限结合使用。 例如假设在JVM超出其内存分配之前可以处理的最大传输中请求为1000。 如果我们预算每个请求的时间不超过30秒那么我们应该期望在最坏的情况下每秒处理不超过33个请求。 但是如果一切正常并且请求仅用500毫秒即可处理则应用程序每秒只能在1000个线程上处理2000个请求。 指定可以使用队列来消除短暂的延迟突发也可能是合理的。 为什么要麻烦 如果线程池中的线程太少则存在以下风险资源利用不足并不必要地将用户拒之门外。 但是如果允许太多线程则会发生资源耗尽这可能会造成更大的破坏。 不仅会耗尽本地资源还可能对其他资源产生不利影响。 例如多个应用程序查询同一个后端数据库。 数据库通常对并发连接数有硬性限制。 如果一个行为异常的无限制应用程序消耗了所有这些连接它将阻止其他应用程序访问数据库。 造成大范围的中断。 更糟糕的是可能会发生级联故障。 想象一下一个环境其中有一个应用程序的多个实例位于一个公共负载平衡器的后面。 如果由于过多的正在进行中的请求而使其中一个实例的内存不足则JVM将花费更多时间进行垃圾收集并减少处理请求的时间。 这种减慢速度将降低该实例的容量并迫使其他实例处理更高比例的传入请求。 随着他们现在使用无限制的线程池处理更多请求会发生相同的问题。 它们耗尽了内存然后再次开始积极地进行垃圾收集。 这个恶性循环在所有实例之间级联直到出现系统性故障。 我经常观察到没有进行负载测试并且允许任意数量的线程。 在通常情况下应用程序可以使用少量线程以传入速率愉快地处理请求。 但是如果处理请求取决于远程服务并且该服务暂时变慢则W的增加平均处理时间的影响会很快耗尽池。 由于从未对应用程序进行最大数量的负载测试因此会出现之前概述的所有资源耗尽问题。 多少个线程池 在微 服务或面向服务的体系结构 SOA中访问多个远程后端服务是正常的。 此设置特别容易发生故障因此应仔细解决这些问题。 如果远程服务的性能下降则可能导致线程池Swift达到其极限从而丢弃后续请求。 但是并非所有请求都可能需要此不正常的后端但是由于线程池已满因此这些请求被不必要地删除了。 通过提供特定于后端的线程池可以隔离每个后端的故障。 在这种模式下仍然只有一个请求工作程序池但是如果请求需要调用远程服务则工作将转移到该后端的线程池。 这使主请求池不会受到单个缓慢后端的负担。 这样只有需要特定后端池的请求才会在故障时受到影响。 多个线程池的最后一个好处是它有助于避免某种形式的死锁。 如果由于尚未处理的请求而导致每个可用线程都被阻塞则将发生死锁并且没有线程能够前进。 当使用多个池并充分了解它们执行的工作时可以在某种程度上缓解此问题。 截止日期和其他最佳做法 常见的最佳做法是确保所有远程呼叫都有最后期限。 也就是说如果远程服务在合理时间内没有响应则该请求将被放弃。 可以在线程池中使用相同的技术。 具体来说如果线程正在处理一个请求的时间超过了定义的期限则应终止该线程。 为新请求腾出空间并在W上设置上限。这似乎是一种浪费但是如果用户通常可能是Web浏览器正在等待响应则30秒后浏览器可能只会给出无论如何还是用户可能变得不耐烦并导航离开。 快速失败是在为后端创建池时可以采用的另一种方法。 如果后端发生故障则线程池将Swift填充等待连接到无响应后端的请求。 相反可以将后端标记为不正常所有后续请求都可能立即失败而不是不必要地等待。 但是请注意需要一种机制来确定后端何时再次恢复健康。 最后如果一个请求需要独立地调用多个后端则应该可以并行而不是顺序地调用它们。 这将减少等待时间但以增加线程为代价。 幸运的是有一个很棒的库hystrix 它打包了许多这些最佳实践并以简单安全的方式公开了它们。 结论 希望本文能增进您对线程池的了解。 通过了解应用程序的需求并结合使用最大线程数和平均响应时间可以确定适当的线程池。 这不仅可以避免级联故障而且可以帮助计划和配置服务。 即使您的应用程序可能未显式使用线程池但它们还是被应用程序服务器或更高级别的抽象隐式使用。 Tomcat JBoss Undertow Dropwizard都为其线程池执行servlet的池提供了多个可调参数。 翻译自: https://www.javacodegeeks.com/2015/12/importance-tuning-thread-pools.html动态调整线程池
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/88174.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!