Java线程池的正确使用方式——不要再new Thread了

4,583

这是我参与更文挑战的第2天,活动详情查看: 更文挑战

背景

最近在实验室做相关工作时,一个小伙伴看见项目代码中出现了 new Thread(...) ,破口大骂之。看见这一场景,我默默地删掉了我在另一个地方写的 new Thread(...) 当作无事发生(还好他没看见XD)。

为了不再犯这种错误,我写下这篇文章来记录一下Java线程究竟该怎么使用(才不会被骂),也是开了一个新坑!

如有错误欢迎联系我指正!

为什么不要用new Thread

首先从我秉持的原则入手,“简洁优雅”。试想如果在一段代码中你需要创建很多线程,那么你就不停地调用 new Thread(...).start() 么?显然这样的代码一点也不简洁,也不优雅。初次之外这样的代码还有很多坏处:

  1. 每次都要新建一个对象,性能差;
  2. 建出来的很多个对象是独立的,缺乏统一的管理。如果在代码中无限新建线程会导致这些线程相互竞争,占用过多的系统资源从而导致死机或者 oom
  3. 缺乏许多功能如定时执行、中断等。

从这些坏处很容易可以看出解决方法,那就是弄一个监管者来统一的管理这些线程,并将它们存到一个集合(或者类似的数据结构)中,而且还要动态地分配它们的任务。当然Java已经给我们提供好十分健全的东西来使用了,那就是线程池

Java线程池

Java提供了一个工厂类来构造我们需要的线程池,这个工厂类就是 Executors 。这个类提供了很多方法,我们这里主要讲它提供的4个创建线程池的方法,即

  • newCachedThreadPool()
  • newFixedThreadPool(int nThreads)
  • newScheduledThreadPool(int corePoolSize)
  • newSingleThreadExecutor()

newCachedThreadPool()

这个方法正如它的名字一样,创建缓存线程池。缓存的意思就是这个线程池会根据需要创建新的线程,在有新任务的时候会优先使用先前创建出的线程。也就是说线程一旦创建了就一直在这个池子里面了,执行完任务后后续还有任务需要会重用这个线程,若是线程不够用了再去新建线程

以一段代码做个例子:

ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); // 创建缓存线程池

for (int i = 0; i < 10; i++) {
    final int index = i;

    // 每次发布任务前等待一段时间,如1s
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    // 执行任务
    cachedThreadPool.execute(() -> System.out.println(Thread.currentThread().getName() + ":" + index));
}

在这个例子里,我在每次调用线程执行任务之前都等待1秒,这使时间让线程池内的线程执行完上一个任务绰绰有余,所以你会发现输出里都是同一个线程在执行任务。

cachepool1.png

ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); // 创建缓存线程池

for (int i = 0; i < 10; i++) {
    final int index = i;

    // 每次发布任务前根据奇偶不同等待一段时间,如1s
    if (i % 2 == 0) {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 执行任务
    cachedThreadPool.execute(() -> System.out.println(Thread.currentThread().getName() + ":" + index));
}

这个例子中我在每次调用线程执行任务之前根据奇偶不同控制其是否等待,这样就会在同一时间需要执行2个任务,所以线程池中按需要多创建了一个线程。你也可以把这个模数改大到3、4、5...来观察线程池是否按需创建了新线程。

cachepool2.png

注意这里的线程池是无限大的,我们并没有规定他的大小。(但其实在实际使用时不可能是无限大的,我会在这个系列后面的文章再来探讨这个问题)

newFixedThreadPool(int nThreads)

可以看到这个方法中带了一个参数,这个方法创建的线程池是定长的,这个参数就是线程池的大小。也就是说,在同一时间执行的线程数量只能是 nThreads 这么多,这个线程池可以有效的控制最大并发数从而防止占用过多资源。超出的线程会放在线程池的一个队列里等待其他线程执行完,这个队列也是值得我们去好好研究的,它是一个无界队列,我会在这个系列后面的文章探讨它。

以一段代码做个例子:

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3); // 创建缓存线程池,大小为3

for (int i = 0; i < 10; i++) {
    final int index = i;

    // 执行任务
    fixedThreadPool.execute(() -> {
        System.out.println(Thread.currentThread().getName() + ":" + index);

        // 模拟执行任务耗时1秒
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

这个例子里可以看到我创建了一个大小为3的线程池,也就是说它支持的最大并发线程数是3,运行后发现这些数确实是3个3个为一组输出的。

fixedpool1.png

合理的设置定长线程池的大小是一个很重要的事情。

newScheduledThreadPool(int corePoolSize)

从 Scheduled 大概可以猜出这个线程池是为了解决上面说过的第3个坏处,也就是缺乏定时执行功能。这个线程池也是定长的,参数 corePoolSize 就是线程池的大小,即在空闲状态下要保留在池中的线程数量。

而要实现调度需要使用这个线程池的 schedule() 方法 (注意这里要把新建线程池的返回类 ExecutorService 改成 ScheduledExecutorService 噢)

以一段代码做个例子:

// 注意!这里把 ExecutorService 改成了 ScheduledExecutorService ,否则没有定时功能
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3); // 创建缓存线程池

// 执行任务
scheduledThreadPool.schedule(() -> System.out.println(Thread.currentThread().getName() + ": 我会在3秒后执行。"),
        3, TimeUnit.SECONDS);

这个例子会在3秒后输出结果。当然你可以根据不同的需求设置不同的定时,甚至还能实现定期执行功能,详细可以查看[官方api]

scheduledpool1.png

newSingleThreadExecutor()

这个线程池就比较简单了,他是一个单线程池,只使用一个线程来执行任务。但是它与 newFixedThreadPool(1, threadFactory) 不同,它会保证创建的这个线程池不会被重新配置为使用其他的线程,也就是说这个线程池里的线程始终如一。

以一段代码做个例子:

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); // 创建单线程池

for (int i = 0; i < 10; i++) {
    final int index = i;

    // 执行任务
    singleThreadExecutor.execute(() -> {
        System.out.println(Thread.currentThread().getName() + ":" + index);

        // 模拟执行任务耗时1秒
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

可以看到输出里他只会一秒一秒地打印内容,只有一个线程在执行任务。

singlethreadpool1.png

线程池的关闭

如果你运行了我上面的示例,你会发现程序一直都没有结束,这是因为我上面的示例代码并没有关闭线程池。线程池本身提供了两个关闭的方法:

  • shutdown() : 将线程池状态置成 SHUTDOWN,此时不再接受新的任务等待线程池中已有任务执行完成后结束
  • shutdownNow() : 将线程池状态置成 SHUTDOWN,将线程池中所有线程中断(调用线程的 interrupt() 操作),清空队列,并返回正在等待执行的任务列表

并且它还提供了查看线程池是否关闭和是否终止的方法,分别为 isShutdown()isTerminated()

总结

那么根据需要使用以上四种线程池就足够应对平时的需求了,别再使用 new Thread(...) 这种方法啦!

当然,线程池只能隐式的控制线程变量,如果有业务需求需要对线程进行定制化的监控控制,那也请毫不吝啬的使用new Thread(...)