likes
comments
collection
share

Java 线程池的基本介绍和使用

作者站长头像
站长
· 阅读数 14

在 Java 中,可以利用线程做很多事情,创建多个线程来高效的完成任务,例如 Tomcat 用多个线程来接收和处理请求,我们也可以创建多个线程来批量处理数据,原本串行执行的任务变成并行执行,充分利用 CPU 多个核的性能。

我们可以用这样的方式创建线程执行并发任务:

for (int i = 0; i < 任务数量; i++) {
    Thread thread = new Thread(任务);
    thread.start();
}

这样子确实能够并发完成任务,但是也带了问题:创建的线程数量不可控制,一个任务就创建一个线程,当任务量庞大的时候,会带来极大的内存开销,反复创建、销毁线程。

通过使用线程池的方式,就可以集中管理线程资源,线程池能够给我们带来如下优点:

  • 复用线程。利用一定数量的线程,反复执行任务,而不用频繁的创建、销毁线程。
  • 控制了资源的总量,合理利用 CPU 和内存。由于复用线程,CPU 和 内存相较于创建多个线程来说占用更低。
  • 统一管理资源,统一停止线程。相同任务类型的线程被统一管理起来,能够在某些情况统一的停止这些任务。

⭐ 在实际开发中,如果需要创建超过五个线程执行类似的任务,就可以考虑使用线程池了。

🚀 创建线程池

我们已经知道了线程池的优点和强大了,现在再介绍一下线程池要怎么去创建,怎么去使用。

🛫 线程池创建线程的规则

首先,线程池有几个核心属性,分别是:

  • corePoolSize,核心线程数量,线程池的线程数量会维持在这个数字上
  • maximumPoolSize,最大线程数量,创建的线程数量不会超过这个数字
  • keepAliveTime,线程存活时间,超过核心线程数的线程,如果空闲时间超过指定时间,就会被回收
  • 任务队列,用于接收、存储待执行的任务,当线程空闲下来时,会从任务队列中取出任务并执行
    • 直接交接队列(SynchronousQueue),队列大小为零,新任务直接开始运行,不会等待
    • 有界队列(ArrayBlockQueue),任务队列是有限的
    • 无界队列(LinkedBlockQueue),任务队列是无限的,理论上可以添加任意数量的任务

线程池创建线程的规则是这样的:

  1. 如果当前线程数<核心线程数当前线程数 < 核心线程数当前线程数<核心线程数,则接受新任务就创建新线程
  2. 如果核心线程数≤当前线程数<最大线程数核心线程数 \le 当前线程数 < 最大线程数核心线程数当前线程数<最大线程数,则接收新任务时,将新任务加入到任务队列中
  3. 如果任务队列满了,并且 当前线程数<最大线程数当前线程数 < 最大线程数当前线程数<最大线程数,则创建新的线程执行任务
  4. 如果任务队列满了,并且 当前线程数=最大线程数当前线程数 = 最大线程数当前线程数=最大线程数,那么拒绝该任务并抛出拒绝任务异常RejectedExecutionException

Java 线程池的基本介绍和使用

🛫 使用 Executors 创建线程池

上文已经提到了线程池的几个核心属性以及创建线程的规则,其实这些核心属性是创建线程池对象的参数,主要来自于ThreadPoolExecutor类的构造方法:

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

我们可以利用这个方法来创建一个线程池:

public static void main(String[] args) {  
    // 创建一个线程池  
    ThreadPoolExecutor pool = new ThreadPoolExecutor(5, 5, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<>());  
  
    // 创建 10 个任务,打印线程名  
    for (int i = 0; i < 10; i++) {  
        // ThreadPoolExecutor 的 execute 方法可以接收并执行任务  
        // 接收 Runnable 接口实现类,Runnable 是一个方法接口,因此可以使用 Lambda 表达式  
        pool.execute(()-> {  
            System.out.println(Thread.currentThread().getName());  
        });  
    }  
}

输出结果:

pool-1-thread-1
pool-1-thread-4
pool-1-thread-3
pool-1-thread-3
pool-1-thread-3
pool-1-thread-3
pool-1-thread-3
pool-1-thread-2
pool-1-thread-5
pool-1-thread-1

阿里手册里面推荐我们给一个线程池指定一个线程工厂,从而另线程池创建的每个线程的名字都有意义。在使用这个方法创建线程的时候,可以这样创建,以指定具体的线程名:

// 创建一个线程工厂,实现接口 ThreadFactory
public class MyThreadPoolFactory implements ThreadFactory {  
    private final AtomicInteger threadNum = new AtomicInteger(1);  
    private final String prefixName;  
  
    public MyThreadPoolFactory(String prefixName) {  
        this.prefixName = prefixName;  
    }  
  
    @Override  
    public Thread newThread(Runnable r) {  
        Thread t = new Thread(r, prefixName + threadNum.getAndIncrement());  
        return t;  
    }  
}

// 修改上面的例子,创建线程池的语句修改为:
ThreadPoolExecutor pool = new ThreadPoolExecutor(5, 5, 1, TimeUnit.SECONDS,  
        new LinkedBlockingQueue<>(), new MyThreadPoolFactory("我的线程"));
// 再次运行查看结果

运行结果:

我的线程2
我的线程5
我的线程3
我的线程1
我的线程3
我的线程5
我的线程2
我的线程4
我的线程3
我的线程1

也可以参考Java线程池中设置线程名称三种方式 - 屠城校尉杜 - 博客园 (cnblogs.com),用现有的工具类创建。

我们也可以使用 Executor 类快速创建各种类型的线程池,可以创建如下类型的线程池:

线程类型说明
固定数量的线程池创建一个线程池,其核心线程数和最大线程数都相同
单线程线程池创建一个线程池,其最多只有一个线程
缓存线程池任务队列使用直接交接队列 最大线程数为整形最大值
定时任务线程池可以定时执行任务或者周期执行任务

我们来看看 Executor 的用法,首先是看看固定数量的线程池的创建方法

public static void main(String[] args) {  
    ExecutorService pool = Executors.newFixedThreadPool(5, new MyThreadPoolFactory("固定线程池"));  
    for (int i = 0; i < 10; i++) {  
        pool.submit(() -> {  
            System.out.println(Thread.currentThread().getName());  
        });  
    }  
}

// 运行结果如下:
固定线程池3
固定线程池5
固定线程池5
固定线程池4
固定线程池1
固定线程池2
固定线程池1
固定线程池4
固定线程池3
固定线程池5

// 方法实际调用情况,还是用到了我们上面提到的方法:
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {  
    return new ThreadPoolExecutor(nThreads, nThreads,  
                                  0L, TimeUnit.MILLISECONDS,  
                                  new LinkedBlockingQueue<Runnable>(),  
                                  threadFactory);  
}

此外,其他类型的线程池创建方法也大同小异,具体可以看看下面的Executors的相关方法,使用起来还是很简单的。

// 创建一个缓存线程池
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {  
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,  
                                  60L, TimeUnit.SECONDS,  
                                  new SynchronousQueue<Runnable>(),  
                                  threadFactory);  
}

// 创建一个单线程线程池
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {  
    return new FinalizableDelegatedExecutorService  
        (new ThreadPoolExecutor(1, 1,  
                                0L, TimeUnit.MILLISECONDS,  
                                new LinkedBlockingQueue<Runnable>(),  
                                threadFactory));  
}

// 创建一个定时任务线程池,这里其实还是调用了 new ThreadPoolExecutor(),可以进源码查看,任务队列使用延时队列
public static ScheduledExecutorService newScheduledThreadPool(  
        int corePoolSize, ThreadFactory threadFactory) {  
    return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);  
}
public ScheduledThreadPoolExecutor(int corePoolSize,  
                                   ThreadFactory threadFactory) {  
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,  
          new DelayedWorkQueue(), threadFactory);  
}
// ScheduledThreadPoolExecutor 的 schedule 方法可以指定任务在一定时间后开始运行,运行结束后任务结束
// scheduleAtFixedRate 方法则可以让一个任务以指定的周期运行,运行结束后会在指定时间后继续运行

🛫 推荐的方法

阿里的Java开发手册是推荐我们使用 ThreadPoolExecutor 来创建线程池,并且指定线程工厂从而让每个线程都有自己的具体名字的。因此,在实际开发过程中,我们可以尽量使用 ThreadPoolExecutor 创建线程池。

🛫 指定合适的线程数量

创建线程池指定合适的线程数量对于应用的性能也很有影响,线程数量分配少了,则性能有限,线程数量分配多了,则浪费了资源。但是要如何确定线程需要的数量呢?

  • 如果是CPU密集型的应用,例如加密、哈希计算、大量计算的任务,可以考虑使用 1-2 被CPU核心数量的线程数。
  • 如果是IO密集型的应用,例如需要反复读写磁盘的任务,则可以指定尽可能多倍于CPU核心数的线程数,因为大多数后线程其实是在等待磁盘 IO 的,可以让其他线程使用资源。 还有一条经验法则,可以利用这条公式设置线程数:线程数=CPU核心数×(1+平均等待时间平均工作时间)线程数=CPU核心数 \times (1 + \frac{平均等待时间}{平均工作时间})线程数=CPU核心数×(1+平均工作时间平均等待时间) 总而言之,线程数的设置没有一个通用的规则,而是根据具体的应用场景不断调试出合适的数量。

🚀 停止线程池

在使用线程池的过程中,我们也并不是想要让线程池一直运行下去的,有时候可能根据业务需要,例如项目需要紧急停止、用户需要暂停等等,我们需要让一个正在运行并且正在执行任务的线程池停止下来。 那么,具体可以怎么停止一个线程池呢?可以看看下面的代码:

public class ThreadPoolDemo implements Runnable {  
  
    @Override  
    public void run() {  
        try {  
            Thread.sleep(500);  
        } catch (InterruptedException e) {  
            throw new RuntimeException(e);  
        }  
    }  
  
    public static void main(String[] args) throws InterruptedException {  
        ExecutorService pool = Executors.newFixedThreadPool(5, new MyThreadPoolFactory("演示停止线程池"));  
        for (int i = 0; i < 50; i++) {  
            pool.submit(new ThreadPoolDemo());  
        }  
  
        // 查看线程池是否处于 shutdown 状态  
        System.out.println("线程池 shutdown:" + pool.isShutdown());  
        System.out.println("停止线程池");  
        pool.shutdown();  
        // 线程池已经进入 shutdown 状态,但是应用还在运行,线程池中的任务还在执行  
        System.out.println("线程池 shutdown:" + pool.isShutdown());  
  
        // 提交新任务会报异常  
        //pool.submit(new ThreadPoolDemo());  
  
        // 如果想要立即停止线程池,可以使用 shutdownNow,会返回线程池中未执行完毕的任务列表  
        // List<Runnable> runnables = pool.shutdownNow();  
  
        // 查看线程池是否已经终止  
        System.out.println("线程池 terminated:" + pool.isTerminated());  
        // 等待线程池中的任务运行完毕,主线程阻塞指定时间  
        pool.awaitTermination(1, TimeUnit.MINUTES);  
        System.out.println("线程池 terminated:" + pool.isTerminated());  
    }
}

// 打印结果:
线程池 shutdown:false
停止线程池
线程池 shutdown:true
线程池 terminated:false
线程池 terminated:true

上面的例子已经演示了如何停止一个正在运行的线程池,现在总结一下上面相关方法的区别:

方法说明
shutdown停止线程池,线程池并不会马上停止。拒绝接受新任务,如果提交新任务会抛出异常
shutdownNow立即停止线程池,正在执行任务的线程会收到中断通知。返回任务队列中的任务
isShutdown线程池是否收到了 shutdown 指令
isTerminated线程池是否已经停止
awaitTerminated调用该方法的线程阻塞住,等待指定时间,运行结果如下:如果所有任务执行完毕,返回 true。 超过了等待时间,返回false。 当前线程被中断,抛出中断异常

🛫 为线程池指定拒绝策略

线程池在以下这些情况会拒绝任务:

  • 线程池的状态是 shutdown,此时提交任务会执行拒绝策略
  • 线程池的任务队列已满,并且线程数量已经到达最大线程数,此时提交任务会执行拒绝策略

可以指定的拒绝策略如下,具体可以查看接口RejectedExecutionHandler 的实现类:

策略说明
AbortPolicy拒绝任务并直接抛出异常,默认策略
DiscardPolicy默默丢弃任务,不抛出异常
CallerRunsPolicy拒绝任务,并让提交任务的线程运行被拒绝的任务
DiscardOldestPolicy丢弃最早提交的任务,然后重新尝试接收该任务

可以在 ThreadPoolExecutor 的这个构造方法中指定拒绝策略:

public ThreadPoolExecutor(int corePoolSize,  
                          int maximumPoolSize,  
                          long keepAliveTime,  
                          TimeUnit unit,  
                          BlockingQueue<Runnable> workQueue,  
                          ThreadFactory threadFactory,  
                          RejectedExecutionHandler handler);
// 例如 new ThreadPoolExecutor(2,5,1,...,new ThreadPoolExecutor.CallerRunsPolicy());

🚀 线程池的状态

从上文中也可以看出,线程池有着各种状态,或者可以提交并执行任务,或者不再接受任务但是仍然执行运行中的任务,或者直接中断执行的任务…… 下面是线程池的各种状态:

状态说明
RUNNING线程池创建后的状态,接收并执行任务
SHUTDOWN拒绝接收新任务,等待任务执行完成,shutdown() 执行后的状态
STOP拒绝接收新任务,中断执行中的任务,返回任务队列中的任务,shutdownNow() 执行后的状态
TIDYING任务已经停止,没有工作线程,执行线程池的 terminated() 方法
TERMINATED线程池已经停止