Java并发编程线程池的创建及使用

160 阅读7分钟

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

在编程中,如果我们需要用到多线程来处理数据提高处理的效率时,因为资源的有限性,不能够无限的使用new Thread来创建线程,如果这样使用不当会造成OOM,这个时候我们就需要用到多线程来控制我们执行的线程个数。对于线程池的使用基本上很多地方都会使用,合理的使用线程池能够给我们带来一定的好处

  1. 降低系统资源消耗,通过重用已存在的线程,不需要不停的创建及销毁线程,降低线程创建和销毁造成的消耗
  2. 提高系统响应速度,当有任务需要执行时,通过复用已存在的线程,无需等待新线程的创建便能够立即执行
  3. 方便线程并发数的管控,因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM
  4. 节省cpu切换线程的时间成本(需要保持当前执行线程的现场,并恢复要执行线程的现场)
  5. 提供更强大的功能,例如:延时定时线程池

创建线程池

在对线程池进行深入的了解时,我们先来看下如何使用线程池,线程池的创建可以通过ThreadPoolExecutor来创建一个线程池,创建线程池的代码如下:

public static void main(String[] args) {
    ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingDeque());
    executor.execute(() -> System.out.println("线程池执行"));
}

从上面的的案例中,new ThreadPoolExecutor的构造参数比较多,对于构造函数,我们需要传入7个参数才能够创建一个线程池,在这里先对7个参数的含义进行解读,先看最基础的构造函数,其他构造函数都调用的是这个构造函数,至于对于一些参数,系统自动帮你赋值

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

corePoolSize

线程池的基本大小,当提交一个任务到线程池的时候,如果线程池的基本线程没有达到线程池的基本大小(例如:基本大小设置为5,此时基本线程个数为3),这时也会创建一个基本线程来执行我们提交的任务,此时基本线程个数为5,直到基本个数等于线程池的基本大小后,才不会创建新的基本线程,而是等待其他任务执行完成后在执行。如果我们在使用的时候调用了prestartAllCoreThreads()方法,线程池则会提交将所有的基本线程任务都创建好,并且都会启动。

maximumPoolSize

线程池最大数量,线程池允许创建的最大线程数。在提交任务的时候,发现正在执行的基本线程数已经等于线程池的基本大小时,线程池会将执行的任务存到队列中等待任务执行,如果存入时发现队列已经满了,并且创建的线程数小于最大线程数,则线程池会继续的创建新的线程执行任务。如果设置的队列大小为无界队列,这个参数就没有用处,因为队列不会满,就不会触发这个参数的机制。

keepAliveTime

线程保持存活的时间,线程池里的工作线程空闲后,保存存活的时间,如果达到最大的存活时间后,线程池就会停掉对应的工作线程,等到需要的时候在创建。所以我们在使用的时候需要正确的设置这个参数值,对于需要执行很多任务的场景并且每个任务的执行时间比较短,这个时候就需要我们调大线程的存活时间,以免频繁的创建线程的开销,提高线程的利用率。

unit

keepAliveTime存活时间的单位

workQueue

任务队列,当提交任务时,工作线程已经满,则需要放入任务队列中等待任务的执行。有下面的几个阻塞队列可以使用:

  • ArrayBlockingQueue:基于数据结构的阻塞队列,此队列按照FICO先进先出原则对任务进行排序
  • LinkedBlockingQueue:基于链表结构的阻塞队列,也是按照FICO原则,吞吐量高于ArrayBlockingQueue
  • SynchronousQueue:不存储元素的阻塞队列,插入数据时必须等到另一个线程调用移除操作才能够插入,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue
  • PriorityBlockingQuere: 具有优先级的无限阻塞队列,根据线程的优先级进行排序,优先级高的任务先执行

threadFactory

用于创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置名字。

handler

饱和拒绝策略,当队列和线程池都满了,该线程池处于饱和状态下就会触发这个机制,有下面4种策略

  • AbortPolicy:直接抛出异常,终止整个线程池的执行
  • CallerRunsPolicy:只用调用者所在线程来运行任务,就是使用主线程来调用任务
  • DiscardOldestPolicy:丢弃队列里最近的一个任务后,然后执行当前任务
  • DiscardPolicy:将任务直接丢掉,不处理

该策略可以自己来实现,只需要实现接口:RejectedExecutionHandler就可以,可以对丢掉的任务日志持久化操作。

提交任务到线程池

在线程池创建完成后,我们需要将需要执行的任务提交给线程池交给线程池来执行,有两种方式可以提交任务,分别是execute()和submit()。这两个方法作用效果都一样,都是提交任务给线程池,只是对任务的返回值有一定的区别。

execute

当执行完成任务不需要返回值,使用execute来提交任务

submit

需要返回值时则采用submit来提交任务,因为提供了实现Callable接口来创建任务,提交任务成功后,会返回Future对象,然后可以获取到执行的结果。

关闭线程时

由于线程池里的任务为阻塞任务,当线程池的任务执行全部执行完成后,我们不需要该线程池时,就需要关闭线程池,也提供了两个方法:shutdown()和shutdownNow(),这两个方法都是关闭线程池,唯一的区别就是:

shutdown

shutdown关闭时,会使任务都全部执行完成后才关闭

shutdownNow

而shutdownNow会立即关闭掉线程池,无论线程池里的任务是否都执行完毕。

监控线程池

当我们提交任务到线程池后,有时候需要对线程的相关指标执行监控,方便在出现问题后,可以根据线程池的相关指标进行定位,并且优化我们后续的操作。可以使用下面提供的方法来获取线程池运行的相关参数。

getTaskCount

获取线程池需要执行的任务数量

getCompletedTaskCount

获取线程池已经完成的任务数量

getLargestPoolSize

获取线程池里曾经创建过的最大线程数量。通过这个数据可以判断线程池是否曾经满过

getPoolSize

获取线程池的线程数量

getActiveCount

获取当前活动的线程数

我们在使用线程池的时候,要合理的使用线程池,并不是设置的越多越好,要根据CPU以及其他因素来决定线程池执行任务的个数。 上面都是关于线程池使用的相关方法,线程池的原理在下篇文章中进行分析,我们知道线程池的原理后,可以更加让我们得心应手的使用线程池,能够最大的发挥线程池的作用,对于出现的问题也能够快速的排查定位。