【问题标题】:Short but frequent jobs: HandlerThread or ThreadPoolExecutor?短而频繁的工作:HandlerThread 还是 ThreadPoolExecutor?
【发布时间】:2019-11-29 07:31:27
【问题描述】:

首先,我无法确定标题应该是什么,所以如果它不够具体,问题本身就是。

我们有一个应用程序使用前台服务并永远保持活动状态,在此服务中,有频繁的数据库访问作业、网络访问作业等等,需要在后台线程上运行。一项工作本身消耗的时间很少,但工作本身却很频繁。显然,它们需要在工作线程上运行,所以我在这里问我们应该遵循哪种设计。

HandlerThread 是一种结构,它创建单个线程并使用队列来执行任务,但始终循环并等待消耗功率的消息,而ThreadPoolExecutor 为每个作业创建多个线程并在作业完成时删除线程,但是由于线程过多,可能会出现泄漏,甚至内存不足。作业计数可能是 5 个,也可能是 20 个,这取决于用户如何以某种方式操作。而且,在 2 个工作之间,可能会有 5 秒或一天的间隔,这完全取决于用户。但是,请记住,应用程序永远存在并等待这些作业执行。

那么,对于这个特定的场合,哪个更好用?线程池执行器还是处理程序线程?任何建议表示赞赏,谢谢。

【问题讨论】:

    标签: java android multithreading


    【解决方案1】:

    警告:我不做 Android 工作,所以我不是那里的专家。我在这里的观点是基于对 Android 文档的快速阅读。

    tl;博士

    ➥ 使用执行器而不是HandlerThread

    Executors 框架比 HandlerThread 使用的旧版 Thread 工具更现代、更灵活、更强大。你可以在 HandlerThread 中做的所有事情,你都可以用 executors 做得更好。

    区别

    HandlerThreadThreadPoolExecutor 之间的一大区别是前者来自 Android,而后者来自 Java。因此,如果您将使用 Java 进行其他工作,您可能不想养成使用 HandlerThread 的习惯。

    另一个很大的区别是年龄。 android.os.HandlerThread 类继承自 java.lang.Thread,并且可以追溯到最初的 Android API 级别 1。虽然在当时还不错,但 Java 中的 Thread 工具在设计上是有限的。后来的 Java 中更现代、更灵活、更强大的 Executors framework 取代了该工具。

    执行者

    您的问题不清楚这些是经常性工作还是偶尔安排的工作。两者都可以用 Executors 处理。

    对于在特定时间运行一次的作业以及重复的计划作业,请使用ScheduledExecutorService。您可以通过指定延迟(等待执行的时间跨度)来安排作业在特定时间运行一次。对于重复的作业,您可以指定等待、然后运行、然后等待、然后运行等的数量。我不会进一步解决这个问题,因为您似乎在谈论零星的直接工作,而不是计划或重复的工作。如果有兴趣,请搜索 Stack Overflow,因为 ScheduledExecutorService 已经在 Stack Overflow 上被报道过很多次了。

    单线程池

    HandlerThread是一个创建单线程的结构

    如果您想重新创建该单线程行为,请使用 thread pool consisting of only a single thread

    ExecutorService es = Executors.newSingleThreadExecutor() ;
    

    完成你的任务。实现RunnableCallable,使用 (a) 实现任一接口的类,(b) 不定义类,通过 lambda 语法或常规语法。

    常规语法。

    Runnable sayHelloJob = new Runnable()
    {
        @Override
        public void run ( )
        {
            System.out.println( "Hello. " + Instant.now() );
        }
    };
    

    Lambda 语法。

    Runnable sayBonjourJob = ( ) -> System.out.println( "Bonjour. " + Instant.now() );
    

    根据需要向执行器服务提交尽可能多的这些作业。

    es.submit( sayHelloJob ) ;
    es.submit( sayBonjourJob ) ;
    

    注意submit 方法返回一个Future。使用Future 对象,您可以检查计算是否完成、等待其完成或检索计算结果。或者您可以选择忽略上面代码中看到的Future 对象。

    固定线程池

    如果您想要多线程行为,只需使用不同类型的线程池创建您的执行程序。

    fixed thread pool 具有服务于单个提交作业队列(RunnableCallable 对象)的最大线程数。线程继续存在,并在发生故障时根据需要进行替换。

    ExecutorService es = Executors.newFixedThreadPool​( 3 ) ;  // Specify number of threads.
    

    其余代码保持不变。这就是使用ExecutorService 接口的美妙之处:您可以更改执行器服务的实现以获得不同的行为,同时不会破坏调用该执行器服务的代码。

    缓存线程池

    cached thread pool 可能会为您提供更好的服务。不像固定线程池那样立即创建和维护一定数量的线程,这个池只在需要时创建线程,最多创建线程。当一个线程完成并休息一分钟以上时,该线程被终止。正如 Javadoc 所指出的,这对于诸如您的“许多短期异步任务”是理想的。但请注意,可以同时运行的线程没有上限。如果您的应用程序的性质使得您可能经常看到许多作业同时到达的高峰,您可能希望使用缓存线程池以外的其他实现。

    ExecutorService es = Executors.newCachedThreadPool() ;
    

    管理执行器和线程

    但由于线程过多,可能会出现泄漏,甚至内存不足

    程序员和系统管理员的工作是不让生产服务器负担过重。您需要监控生产中的性能。管理很容易执行,因为您可以控制支持执行程序服务的线程池中可用的线程数。

    我们有一个应用程序,它使用前台服务并永远保持活力

    当然,您的应用最终会结束,即关闭。发生这种情况时,请务必关闭执行程序及其支持线程池。否则线程可能会存活下来,并无限期地继续下去。请务必使用应用执行环境的生命周期挂钩来检测应用关闭并对其做出反应。

    作业计数可能是 5 个,也可能是 20 个,具体取决于用户以某种方式执行的操作。

    提交给执行器服务的作业会被缓冲,直到它们可以被安排在线程上执行。因此,您可能有一个线程池,例如,3 个线程和 20 个等待作业。没问题。等待的作业最终将在它们的时间到来时被执行。

    您可能希望优先处理某些工作,以便在优先级较低的工作之前完成。一种简单的方法是拥有 两个 执行器服务。每个执行器都有自己的后台线程池。一个执行器用于较少但优先级较高的作业,而另一个执行器用于许多较低优先级的作业。

    请记住,线程池中的线程在待机时不做任何工作,在 Java 中对于 CPU 或内存而言几乎没有开销。因此,让一个特殊的、优先级更高的执行器服务坐在那里等待最终的工作到达没有任何不利之处。唯一需要担心的是,您的所有后台线程总数及其工作负载不会使您的机器不堪重负。此外,线程池的实现很可能会在闲置一段时间后关闭未使用的线程。

    【讨论】:

    • 我喜欢这个答案,它足够详细,我可以选择执行程序而不是处理程序线程。我分析了我们案例的使用情况,池最多创建了 5 个线程,每个线程都在 60 秒后被杀死,如果不使用,则使用 Executors.newCachedThreadPool()。此外,我选择使用带有ThreadFactory 的守护线程来不挂起虚拟机,以回收它用于杀死这些线程的内存,以防无法调用ExecutorService.shutdown()
    • @FurkanYurdakul 当然,您可能需要采取额外的步骤,使用ThreadFactory。但我建议你避免premature optimization 的风险。在您明确证明存在问题之前,不要采取额外的步骤。现代 JVM 都经过高度调优且性能极佳。
    • 嗯,现代 JVM 可能性能很好,但是,我们的应用程序的目标受众是 5 年前的手机(我的意思是直到 Android 4.2.2),所以一切都需要尽可能地保持优化.而且,不太可能,如果进程突然终止,则无需执行任何操作,因此我认为在执行程序使用的那些线程上使用 Thread.setDaemon(true) 会更安全。
    【解决方案2】:

    不要真的认为这是您正在运行的线程数量的问题,更多的是您希望它们如何运行。如果您希望它们一次运行一个(即您只想一次执行数据库查询),请使用HandlerThread。如果您想要多线程/线程池,请使用和Executor

    根据我的经验,泄漏实际上更多取决于您对线程的编码方式,而不是真正选择的实现。

    就个人而言,我会使用HandlerThread,这是一篇关于实现它们以及如何避免内存泄漏的好文章...Using HandlerThread in Android

    【讨论】:

    • 当这些任务要被执行时,问题就开始了。如问题中所述,它可能是 5 秒的间隔,或 一天 的间隔,这将导致处理程序线程不必要地运行,从而消耗电力。但是,如果间隔仅为 5 秒,那么使用单个线程(例如处理程序线程)将是比为每个作业创建新线程更好的方法。所以,我被难住了。
    • @Furkan,你也可以设置一个机制,如果没有更多的工作可以销毁你的处理程序,并在你需要的时候启动它。我实际上会假设Executor 会使用更多功率,因为​​它需要维护线程池。还可以看看这个专门讨论 Performance 的 Android 开发者页面,它没有谈到电源使用情况,但似乎也表明 HandlerThread 是更好的选择,特别是考虑到您的要求。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-11-12
    • 1970-01-01
    • 1970-01-01
    • 2010-10-04
    • 2014-11-25
    • 1970-01-01
    • 2018-03-21
    相关资源
    最近更新 更多