一、简介
多线程编程是一项复杂的任务,涉及到线程的创建、销毁、资源管理等一系列问题。为了更有效地管理线程,提高程序的性能和可维护性,Java 提供了线程池机制。
线程的频繁创建或者管理不当,会导致内存溢出、线程阻塞等。因此线程池技术显的尤为重要。
线程池是一种线程管理的机制,它可以维护一组线程,用于执行各种任务,而不需要为每个任务都创建和销毁线程。线程池的核心思想是将线程的创建、销毁和管理与任务的提交和执行分离开来,从而降低了线程创建和销毁的开销,提高了系统的性能和稳定性。
二、引子
先从阿里规约提醒说起
当我们创建一个线程池时,规约插件会提醒 “ 要使用带有ThreadFactory参数的ThreadPoolExecutor构造方法哦,这样你就可以方便的设置线程名字啦。 ”
具体对策是:
创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。创建线程池的时候请使用带ThreadFactory的构造函数,并且提供自定义ThreadFactory实现或者使用第三方实现。
三、 线程工厂
创建线程并指定具体的线程名字,方便追溯和分析。ThreadFactory实际上是一个接口,实例化有两种方式,一种是第三方实现,另一种是自定义实现。
第三方实现需要引入依赖
- ThreadFactoryBuilder
Google guava 工具类 提供的 ThreadFactoryBuilder ,使用链式方法创建。
* ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
* .setNameFormat("demo-pool-%d").build();
- CustomizableThreadFactory
Spring 框架提供的 CustomizableThreadFactory。
ThreadFactory springThreadFactory = new CustomizableThreadFactory("springThread-pool-");
- BasicThreadFactory
Apache commons-lang3 提供的 BasicThreadFactory.
ThreadFactory basicThreadFactory = new BasicThreadFactory.Builder()
.namingPattern("basicThreadFactory-").build();
Androidx默认提供
Androidx在Executors给我们提供了一个默认的线程工厂
/**
* The default thread factory.
*/
private static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
自定义实现
也可以根据需要,自定义实现。创建线程的时候,命名线程序号。
public class CustomThreadFactory implements ThreadFactory {
private final AtomicInteger index = new AtomicInteger();
@Override
public Thread newThread(Runnable r) {
// 创建线程,线程id原子类自增
Thread thread = new Thread(r, "线程-core-" + index.incrementAndGet());
Log.d("ThreadPoolUtils", "---------------newThread-------------" + thread.getName());
return thread;
}
四、 线程池
线程工厂搞定之后,再来看下线程池的创建。
线程池创建推荐使用ThreadPoolExecutor,一个完整规范的线程池创建需要指定核心线程数量,最大线程数量,线程存活时间,存活时间单位,线程工厂,拒绝策略。另外还要增加异常处理机制,为了排查分析问题。
高并发任务时的处理,一个适合需求的任务队列,一个合适的拒绝策略,对应用系统的健壮性十分重要。
构造参数的具体含义:
corePoolSize:指定了线程池中的线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去
maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量
keepAliveTime:当线程池中的空闲线程数量超过corePoolSize时,多余的线程会在多长时间内被销毁
unit:keepAliveTime的单位
workQueue:任务队列,被添加到线程池中,但尚未被执行的任务;它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列几种
threadFactory:线程工厂,用于创建线程,一般用默认即可
handler:拒绝策略,当任务太多来不及处理时,如何拒绝任务
创建一个线程池:
// 任务队列
BlockingQueue<Runnable> mBlockQueue = new LinkedBlockingQueue<>(30);
// 线程工厂,需要命名每个线程,方便问题追溯
ThreadFactory nameThreadFactory = new CustomThreadFactory();
// 拒绝策略
RejectedExecutionHandler mRejectedExecutionHandler = new ThreadPoolExecutor.DiscardOldestPolicy();
// 创建线程池,依次传入核心线程数量,最大线程数量,线程存活时间,存活时间单位,线程工厂,拒绝策略
mExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME,
TimeUnit.SECONDS, mBlockQueue, nameThreadFactory,mRejectedExecutionHandler){
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
// 线程池执行完任务后,可以打印当前线程名称,以及计算时间
Log.d("ThreadPoolUtils", "---------------Runnable is done-------------" + Thread.currentThread().getName());
// 线程池异常处理机制
if (t != null) {
Log.d("ThreadPoolUtils", "---------------afterExecute-------------" + t.getMessage());
}
}
};
运行规则
- 如果线程池中的线程数量没有达到核心线程的数量那么会直接启动一个核心线程来执行任务
- 如果线程池中线程数量已经达到或者超过核心线程的数量那么会把后续的任务插入到队列中等待执行
- 如果任务队列也无法插入那么在基本可以确定是队列已满这时如果线程池中的线程数量没有达到最大值就会立刻创建非核心线程来执行任务
- 如果非核心线程的创建已经达到或者超过线程池的最大数量那么就拒绝执行此任务,ThreadPoolExecutor会通过RejectedExecutionHandler抛出异常rejectedExecution
五、 线程池的作用
为什么要使用线程池以及使用Executors创建
在《阿里巴巴java开发手册》中指出了线程资源必须通过线程池提供,不允许在应用中自行显示的创建线程,这样一方面是线程的创建更加规范,可以合理控制开辟线程的数量;另一方面线程的细节管理交给线程池处理,优化了资源的开销。
线程池池不允许使用Executors去创建,而要通过ThreadPoolExecutor方式,这一方面是由于jdk中Executor框架虽然提供了如newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool()等创建线程池的方法,但都有其局限性,不够灵活;另外由于前面几种方法内部也是通过ThreadPoolExecutor方式实现,使用ThreadPoolExecutor有助于大家明确线程池的运行规则,创建符合自己的业务场景需要的线程池,避免资源耗尽的风险。
作用
- 线程池中的线程可重复使用,避免因为线程的创建和销毁带来的性能开销
- 能有效控制线程池中的最大并发数避免大量的线程之间因互相抢占系统资源导致的阻塞现象
- 能够对线程进行简单的管理并提供定时执行以及指定间隔循环执行等功能。
六、拒绝策略
当线程池的线程数达到最大线程数时,需要执行拒绝策略。拒绝策略需要实现 RejectedExecutionHandler 接口。Executors框架提供了四种策略:
- AbortPolicy(默认):终止策略,丢弃任务并抛出 RejectedExecutionException 异常。
CallerRunsPolicy:调用者运行策略超出的线程任务由调用线程处理该任务。
DiscardPolicy:丢弃任务策略,注意是丢弃新提交的任务。
DiscardOldestPolicy:丢弃最早未处理请求策略,丢弃最先进入阻塞队列的任务以腾出空间让新的任务入队列。
七、任务队列
当核心线程没有空闲时,新提交的任务即进入任务队列排队等待。任务队列是基于阻塞队列实现的,即采用生产者消费者模式,在 Java 中需要实现 BlockingQueue 接口。Java提供7种队列的实现:
- ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列(数组结构可配合指针实现一个环形队列)
- LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列,在未指明容量时,容量默认为 Integer.MAX_VALUE
- PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列,对元素没有要求,可以实现 Comparable 接口也可以提供 Comparator 来对队列中的元素进行比较。跟时间没有任何关系,仅仅是按照优先级取任务
- DelayQueue:类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现 Delayed 接口,通过执行时延从队列中提取任务,时间没到任务取不出来
- SynchronousQueue: 一个不存储元素的阻塞队列,消费者线程调用 take() 方法的时候就会发生阻塞,直到有一个生产者线程生产了一个元素,消费者线程就可以拿到这个元素并返回;生产者线程调用 put() 方法的时候也会发生阻塞,直到有一个消费者线程消费了一个元素,生产者才会返回
- LinkedBlockingDeque: 使用双向队列实现的有界双端阻塞队列。双端意味着可以像普通队列一样 FIFO(先进先出),也可以像栈一样 FILO(先进后出)
- LinkedTransferQueue: 它是ConcurrentLinkedQueue、LinkedBlockingQueue 和 SynchronousQueue 的结合体,但是把它用在 ThreadPoolExecutor 中,和 LinkedBlockingQueue 行为一致,但是是无界的阻塞队列。
注意有界队列和无界队列的区别:如果使用有界队列,当队列饱和时并超过最大线程数时就会执行拒绝策略;而如果使用无界队列,因为任务队列永远都可以添加任务,所以设置 maximumPoolSize 没有任何意义。