在Android中,new Thread(task).start()和ThreadPoolExecutor.submit(task)有何区别?

17
在我的Android项目中,我有很多需要异步运行一些代码的地方(例如web请求,调用数据库等)。这不是长时间运行的任务(最多几秒钟)。 到目前为止,我一直通过创建一个新线程,传递一个包含任务的新可运行对象来处理这种情况。但最近我读了一篇关于Java中线程和并发的文章,并明白为每个单独的任务创建一个新线程不是一个好的决定。
所以现在我已经在我的Application类中创建了一个ThreadPoolExecutor,它持有5个线程。 这是代码:
public class App extends Application {

    private ThreadPoolExecutor mPool;

    @Override
    public void onCreate() {
        super.onCreate();

        mPool =  (ThreadPoolExecutor)Executors.newFixedThreadPool(5);
    }
}

同时我也有一种方法可以向执行器提交Runnable任务:

public void submitRunnableTask(Runnable task){
    if(!mPool.isShutdown() && mPool.getActiveCount() != mPool.getMaximumPoolSize()){
        mPool.submit(task);
    } else {
        new Thread(task).start();
    }
}

所以当我想在我的代码中运行一个异步任务时,我获取App的实例并调用submitRunnableTask方法将runnable传递给它。正如您所看到的,我还检查线程池是否有空闲线程来执行我的任务,如果没有,我就创建一个新的线程(我不认为这会发生,但无论如何...我不希望我的任务在队列中等待并减慢应用程序速度)。

在Application的onTerminate回调方法中,我关闭线程池。

所以我的问题是:这种模式比在代码中创建新线程更好吗?我的新方法有什么优缺点?它可能会引起我尚未意识到的问题吗?您能否向我提供比此更好的方式来管理我的异步任务?

P.S. 我在Android和Java方面有一些经验,但远非并发专家:)因此可能有些方面我对这类问题理解不够深刻。任何建议都将不胜感激。


3
线程池避免了为每个新任务创建和销毁线程所需的相当大的开销。此外,它使您能够拥有一些规定在任何给定时刻存在多少工作线程的策略,而不仅仅是让它随意发生。此外,如果你的应用程序需要关闭所有线程,它还可以节省你重新发明机器的时间。 - Solomon Slow
1
我管理几个开源项目,旨在为任务并行和数据并行应用程序提供支持。这里有一篇适用于Android的文章:http://coopsoft.com/ar/AndroidArticle.html - edharned
@edharned,非常感谢您,但我的第一印象是您的框架对于我的情况来说过于复杂。我只需要进行一些小的JSON-RPC请求和数据库查询。虽然我还没有详细调查您的框架,也许我的第一印象是错误的。 - Andranik
3个回答

21

假设您的任务很短,本答案认为这种模式比在代码中创建新线程更好。

这种模式比在代码中创建新线程更好吗?

这样做更好一些,但仍然远非理想。您仍在为短期任务创建线程。相反,您只需要创建不同类型的线程池,例如使用Executors.newScheduledThreadPool(int corePoolSize)

行为上有什么区别呢?

  • FixedThreadPool总是拥有一组要使用的线程,如果所有线程都忙,则将新任务放入队列中。
  • 默认的ScheduledThreadPool通过Executors类创建,它会保留最小数量的空闲线程池。如果所有线程都忙碌并且有新任务进来,则它会为其创建一个新线程,并在任务完成60秒后销毁该线程,除非再次需要它。

第二个选项可以让您无需手动创建新线程。即使没有“Scheduled”部分,也可以实现此行为,但那时您将不得不自己构建执行程序。构造函数是

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue)

各种选项让您可以微调行为。

如果有些任务时间很长...

我的意思是非常长。例如在大部分应用程序生命周期中(实时双向连接?服务器端口?多播监听器?)。在这种情况下,将您的Runnable放入执行者中是不利的 - 标准执行者设计用于处理此类情况,它们的性能将会恶化。

考虑您的固定线程池 - 如果您有5个长时间运行的任务,则任何新任务都将生成一个新线程,完全破坏了池可能的任何收益。如果您使用更灵活的执行器 - 某些线程将被共享,但并不总是。

经验法则是:

  • 如果是短期任务 - 使用执行器。
  • 如果是长期任务 - 确保您的执行者可以处理它(即它没有最大池大小或足够的最大线程以处理1个线程暂时离开)
  • 如果是需要始终与主线程同时运行的并行过程 - 使用另一个线程。

感谢您提供详细的答案。我尝试了ScheduledThreadPool,但似乎当所有线程都忙碌时,它不会创建新线程。我使用2个核心线程创建了它进行测试(ScheduledThreadPoolExecutor)Executors.newScheduledThreadPool(2),然后运行了很多任务,但是getActiveCount()getLargestPoolSize()从未超过2。也许我做错了什么? - Andranik
1
@Andranik,这似乎不是测试线程计数是否增加的好方法。您尝试过提交几个紧密循环的任务吗?而且您必须检查Android是否实际上使用无限池大小创建池-目前我只安装了SE Java。 - Ordous
我还没有尝试过紧密循环,但我认为它不会创建。在Android文档中,我读到了这个“特别是因为它使用corePoolSize线程和无界队列作为固定大小的池,所以对maximumPoolSize的调整没有任何有用的效果。” 老实说,我不太明白他们想要表达什么,但在我看来,它似乎不会创建新的线程,只是将它们放入队列中,直到线程空闲为止。 - Andranik
1
@Andranik 我建议你阅读 ThreadPoolExecutor 的文档,因为它解释了何时创建新线程。特别是它指出:“如果运行的线程数超过 corePoolSize 但少于 maximumPoolSize,则仅在队列已满时才会创建新线程。”由于使用的队列是无界的(根据定义不能满),因此您永远不会创建超过 corePoolSize 线程。我猜您需要构建自己的执行器,其中包含一个元素队列。 - Ordous
1
或者更确切地说,一个SynchronousQueue,因为它的容量为0。 - Ordous
我根据您提供的信息创建了一个不同的设计。如果您感兴趣,请查看下面的帖子。听取一些评论将会非常有趣... - Andranik

7

回答你的问题——是的,使用Executor比创建新线程更好,因为:

  1. Executor提供了多种不同的线程池。它允许重用已经存在的线程,从而增加了性能,因为线程创建是一个昂贵的操作。
  2. 如果一个线程死亡,Executor可以用一个新线程替换它而不影响应用程序。
  3. 更改多线程策略更容易,因为只需要更改Executor实现就可以了。

4

根据Ordous的评论,我修改了代码,使其仅适用于一个池。

public class App extends Application {

    private ThreadPoolExecutor mPool;

    @Override
    public void onCreate() {
        super.onCreate();

        mPool =  new ThreadPoolExecutor(5, Integer.MAX_VALUE, 1, TimeUnit.MINUTES, new SynchronousQueue<Runnable>());
    }
}


public void submitRunnableTask(Runnable task){
    if(!mPool.isShutdown() && mPool.getActiveCount() != mPool.getMaximumPoolSize()){
        mPool.submit(task);
    } else {
        new Thread(task).start(); // Actually this should never happen, just in case...
    }
}

我希望这对其他人有所帮助。如果更有经验的人对我的方法有意见,我将非常感激他们的评论。


2
你已经从最初的问题走了很长的路。在将其作为完整解决方案发布之前,您可能希望将其发布到CodeReview.SE以获取更好的评论。不过,我在这里可以给出一个建议:您可以使用一个池来获得相同的行为:mPool = new ThreadPoolExecutor(5, Integer.MAX_VALUE, 1, TimeUnit.MINUTES, new SynchronousQueue<Runnable>()) - Ordous
1
@Ordous,谢谢。我已经编辑了我的回答。并且我将你的第一个回答标记为正确的答案,因为它更好地回答了我的初始问题。 - Andranik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接