8

我有一些用于 REST 服务的旧 Java 代码,它为每个传入请求使用单独的线程。即主循环将在 socket.accept() 上循环并将套接字移交给一个 Runnable,然后它将启动它自己的后台线程并在其自身上调用 run。这在一段时间内工作得非常好,直到最近我注意到在高负载下接受处理请求的延迟将变得不可接受。当我说得非常好时,我的意思是它每秒处理 100-200 个请求而没有显着的 CPU 使用率。只有当其他守护进程也增加负载并且只有一次负载超过 5 时,性能才会下降。当机器处于来自其他进程组合的高负载(5-8)时,从接受到处理的时间会变得非常长( 500 毫秒到 3000 毫秒),而实际处理保持在 10 毫秒以下。

已经习惯了 .NET 上的线程池,我认为线程创建是罪魁祸首,我想我会在 java 中应用相同的模式。现在我的 Runnable 使用 ThreadPool.Executor 执行(并且池使用和 ArrayBlockingQueue)。同样,它在大多数情况下都非常有效,除非机器负载变高,然后从创建可运行对象到调用 run() 的时间显示出同样荒谬的时间。但更糟糕的是,在线程池逻辑到位的情况下,系统负载几乎翻了一番(10-16)。所以现在我在双倍负载下遇到了同样的延迟问题。

我的怀疑是队列的锁竞争比之前没有锁的新线程启动成本更糟糕。任何人都可以分享他们对新线程与线程池的经验。如果我的怀疑是正确的,那么任何人都有另一种方法来处理没有锁争用的线程池?

我很想将整个系统设为单线程,因为我不知道我的线程有多大帮助,而且 IO 似乎不是问题,但我确实收到了一些长期存在的请求,然后阻止一切。

谢谢,阿恩

更新:我切换到Executors.newFixedThreadPool(100);它,虽然它保持相同的处理能力,但负载几乎立即翻了一番,运行 12 小时显示负载始终保持在 2 倍。我想在我的情况下,每个请求一个新线程更便宜。

4

4 回答 4

12

配置如下:

new ThreadPoolExecutor(10, 100, 30, TimeUnit.SECONDS, 
        new ArrayBlockingQueue<Runnable>(100))

那么一旦有 10 个线程同时处理请求,就将进一步的请求添加到队列中,除非队列中达到 100 个请求,此时会开始创建新的线程,除非已经有 100 个线程,此时该命令的处理将被拒绝。

(复制如下)的javadocsThreadPoolExecutor部分可能值得一读。

基于它们,以及您显然愿意运行 100 个线程,以及您希望接受所有请求并最终处理它们。我建议尝试以下变体:

new ThreadPoolExecutor(100, 100, 0, TimeUnit.SECONDS, 
        new LinkedBlockingQueue<Runnable>())

顺便说一句,这就是你从中得到的Executors.newFixedThreadPool(100);


排队

任何 BlockingQueue 都可以用来传输和保存提交的任务。此队列的使用与池大小交互:

  • 如果运行的线程少于 corePoolSize,则 Executor 总是更喜欢添加新线程而不是排队。
  • 如果 corePoolSize 或更多线程正在运行,Executor 总是更喜欢排队请求而不是添加新线程。
  • 如果请求无法排队,则会创建一个新线程,除非这将超过 maximumPoolSize,在这种情况下,该任务将被拒绝。

排队的一般策略有以下三种:

  1. 直接交接。工作队列的一个很好的默认选择是 SynchronousQueue,它将任务交给线程而不用其他方式保留它们。在这里,如果没有立即可用的线程来运行任务,则尝试将任务排队将失败,因此将构造一个新线程。在处理可能具有内部依赖关系的请求集时,此策略可避免锁定。直接切换通常需要无限的 maximumPoolSizes 以避免拒绝新提交的任务。这反过来又承认了当命令的平均到达速度超过了它们的处理速度时,可能会出现无限线程增长。
  2. 无界队列。当所有 corePoolSize 线程都忙时,使用无界队列(例如没有预定义容量的 LinkedBlockingQueue)将导致新任务在队列中等待。因此,不会创建超过 corePoolSize 个线程。(因此maximumPoolSize的值没有任何影响。)当每个任务完全独立于其他任务时,这可能是合适的,因此任务不会影响彼此的执行;例如,在网页服务器中。虽然这种排队方式在平滑请求的瞬时突发方面很有用,但它承认当命令平均到达速度快于处理速度时,工作队列可能会无限增长。
  3. Bounded queues. A bounded queue (for example, an ArrayBlockingQueue) helps prevent resource exhaustion when used with finite maximumPoolSizes, but can be more difficult to tune and control. Queue sizes and maximum pool sizes may be traded off for each other: Using large queues and small pools minimizes CPU usage, OS resources, and context-switching overhead, but can lead to artificially low throughput. If tasks frequently block (for example if they are I/O bound), a system may be able to schedule time for more threads than you otherwise allow. Use of small queues generally requires larger pool sizes, which keeps CPUs busier but may encounter unacceptable scheduling overhead, which also decreases throughput.
于 2009-03-04T07:34:20.537 回答
4

测量,测量,测量!它把时间花在哪里?创建 Runnable 时必须发生什么?Runnable 是否有任何可能在实例化过程中阻塞或延迟的内容?延迟期间发生了什么?

实际上,我非常相信从总体上考虑问题,但是这种情况,像这样的意外行为,只需要进行一些测量。

什么是运行时环境、JVM 版本和架构?

于 2009-03-01T23:05:50.617 回答
1

Sun 的 实现Thread虽然比以前快得多,但确实具有锁定功能。IIRC,ArrayBlockingQueue忙碌时根本不应该锁定。因此它是分析器时间(甚至只是几秒ctrl-\或几秒jstack)。

系统负载只是告诉你有多少线程排队。它不一定很有用。

于 2009-03-02T12:56:31.933 回答
0

只是用我自己的一些代码做到了这一点。我使用 Netbeans 分析器来转换我正在使用的线程池实现。你应该可以用Visual VM做同样的事情,但我还没有尝试过。

于 2009-03-01T23:43:13.897 回答