Java线程池工作原理浅析

Java线程池工作原理浅析

最近使用了okHttp做网络请求稍微阅读了它的源码,里面涉及到了线程池的概念,来复习一下线程池了。


文章目录


前言

随着项目业务的快速扩张,你是否已经注意到很多单独的线程游离在各个模块中,一旦想做线程方面的监控与优化,代码将需要大动干戈


提示:以下是本篇文章正文内容,下面案例可供参考

一、为什么要用线程池??

  • 线程属于稀缺资源,它的创建会消耗大量的系统资源。
  • 程频繁地销毁,会频繁地触发GC机制,使系统性能降低。
  • 多线程并发执行缺乏统一的管理与监控。

二、线程池的使用

1.线程池的创建使用可通过java并发包中的Executors类完成,它提供了创建线程池的常用方法。

  • newFixedThreadPool
  • newSingleThreadExecutor
  • newCachedThreadPool
  • newScheduledThreadPool

代码如下:

java 复制代码
public void main() {
    ExecutorService executorService = Executors.newFixedThreadPool(3);
    for(int i = 0; i < 20; i++) {
        executorService.execute(new MyRunnable(i));
    }
}

static class MyRunnable implements Runnable {
    int id;
     MyRunnable(int id) {
        this.id = id;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(3000);
            Log.i("threadpool", "task id:"+id+" is running threadInfo:"+Thread.currentThread().toString());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

示例中创建了一个固定线程数的线程池,并向其中添加20个任务。

日志一次打印三条,每3秒打印一次,所有任务都在名为pool-1-thread-1,pool-1-thread-2,pool-1-thread-3的线程中运行,这与我们为线程池设置的大小相吻合。导致这种现象的原因是线程池中只有三个线程,当一次性将20个任务加入到线程池中时,前三个任务优先执行,后面的任务都在等待。

而如果我们把ExecutorService executorService = Executors.newFixedThreadPool(3); 换为ExecutorService executorService = Executors.newCachedThreadPool();

一瞬间任务都执行完了,可以预见使用newCachedThreadPool方式创建的线程池,执行任务时会创建足够多的线程。

2.线程池常见的种类

先上图:

如何创建线程池呢?

java 复制代码
// Executor.newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

//Executor.newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

线程池都是通过ThreadPoolExecutor来创建的。看下它的构造方法

java 复制代码
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        ...
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

构造方法声明的一系列参数,理解了它们线程池的基本原理你就掌握了,我们来看看他们的具体含义:

  • corePoolSize 核心线程数,除非设置核心线程超时(allowCoreThreadTimeOut),线程一直存活在线程池中,即使线程处于空闲状态。
  • maximumPoolSize 线程池中允许存在的最大线程数。
  • workQueue 工作队列,当核心线程都处于繁忙状态时,将任务提交到工作队列中。如果工作队列也超过了容量,会去尝试创建一个非核心线程执行任务。
  • keepAliveTime 非核心线程处理空闲状态的最长时间,超过该值线程则会被回收。
  • threadFactory 线程工厂类,用于创建线程。
  • RejectedExecutionHandler 工作队列饱和策略,比如丢弃、抛出异常等。

线程池创建完成后,可通过execute方法提交任务,线程池根据当前运行状态和特定参数对任务进处理,整体模型如下图:

添加流程:

常见的几种线程池使用的构造参数

线程池类型 核心线程数 最大线程数 非核心线程空闲时间 工作队列
newFixedThreadPool 必须指定 必须指定 0 LinkedBlockingQueue
newSingleThreadExecutor 1 1 0 LinkedBlockingQueue
newCachedThreadPool 0 Integer.MAX_VALUE 60s SynchronousQueue
newScheduledThreadPool 必须指定 Integer.MAX_VALUE 0 DelayedWorkQueue

3.阻塞队列

为什么要用阻塞队列呢?

实际上阻塞队列常用于生产者-消费者模型,任务的添加是生产者,任务的调度执行是消费者,他们通常在不同的线程中,如果使用非阻塞队列,那势必需要额外的处理同步策略和线程间唤醒策略。比如当任务队列为空时,消费者线程取元素时会被阻塞,当有新的任务添加到队列中时需唤醒消费者线程处理任务。

阻塞队列的实现就是在添加元素和获取元素时设置了各种锁操作(Lock+Condition)。

另一个需要关注的是阻塞队列的容量问题,因为根据线程池处理流程图,阻塞队列容量的大小直接影响非核心线程的创建。具体来说,当阻塞队列未满时并不会创建非核心线程,而是将任务继续添加到阻塞队列后面等待核心线程(如果有)执行。

  • LinkedBlockingQueue 内部用链表实现的阻塞队列,默认的构造函数使用Integer.MAX_VALUE作为容量,即常说的"无界",另可以通过带capacity参数的构造函数限制容量。使用Executors工具类创建的线程池容量均为无界的。
  • SynchronousQueue 容量为0,每当有任务添加进来时会立即触发消费,即每次插入操作一定伴随一个移除操作,反之亦然。
  • DelayedWorkQueue 用数组实现,默认容量为16,支持动态扩容,可对延迟任务进行排序,类似优先级队列,搭配ScheduledThreadPoolExecutor可完成定时或延迟任务。
  • ArrayBlockingQueue 它不在上述线程池的体系当中,它基于数组实现,容量固定且不可扩容。

应根据实际需求选择合适的阻塞队列,现在我们再来看这些线程池的使用.

  • newFixedThreadPool 它的特点是没有非核心线程,这意味着即使任务过多也不会创建新的线程,即使任务闲置也仍保留一定数量的核心线程。等待队列无限,性能相对稳定,适用于长期有任务要执行,同时任务量也不大的场景。
  • newSingleThreadExecutor 相当于线程数量为1的newFixedThreadPool,因为线程数量为1,所以用于任务需顺序执行的场景。
  • newCachedThreadPool 它的特点是没有核心线程,非核心线程无限,可短时间内处理无限多的任务,但实际上创建线程十分消耗资源,过多的创建线程极可能导致OOM,同时设置了线程超时时间,还涉及到线程资源的释放,大量任务并行时性能不稳定,少量任务并行且后续不再需要执行其他任务的场景可使用。
  • newScheduledThreadPool 通常用于定时或延迟任务。

在实际开发过程中不建议直接使用Executors提供的方法,如果任务规模、响应时间大致确定,应根据实际需求通过ThreadPoolExecutor各种构造函数手动创建,还自由可控制线程数、超时时间、阻塞队列、饱和策略(默认的饱和策略都是AbortPolicy即抛出异常)。

4.拒绝策略

内置的拒绝策略有如下四种

  • DiscardPolicy 将丢弃被拒绝的任务。
  • DiscardOldestPolicy 将丢弃队列头部的任务,即先入队的任务会出队以腾出空间。
  • AbortPolicy 抛出RejectedExecutionException异常。
  • CallerRunsPolicy 在execute方法的调用线程中运行被拒绝的任务。

5.线程池大小的选择

这需要大致了解任务是CPU密集型还是IO密集型。

  • CPU密集型 比如大量的计算任务,CPU占用率较高,那么此时如果多开线程反而会因为CPU频繁的做线程调度导致性能降低。一般建议线程数为cpu核心数+1,加1是为了防止某个核心线程阻塞或意外中断时作为候补。
  • O密集型 通常指文件I/O、网络I/O等。线程数的选取与IO耗时和CPU耗时的比例有关,最佳线程数 = CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)],之所以设置比例是为了使I/O设备和CPU的利用率都达到最大。

总结

未完!

相关推荐
老骥伏枥~2 小时前
【C# 入门】变量、常量与命名规范
开发语言·c#
2501_944396192 小时前
Flutter for OpenHarmony 视力保护提醒App实战 - 性能优化技巧
android·flutter·性能优化
ANGLAL2 小时前
35.登录认证演进及双token机制
java
毕设源码-朱学姐2 小时前
【开题答辩全过程】以 基于spring boot的摩托车合格证管理系统为例,包含答辩的问题和答案
java·spring boot·后端
2401_832131952 小时前
模板编译期机器学习
开发语言·c++·算法
嵌入小生0072 小时前
Data Structure Learning: Starting with C Language Singly Linked List
c语言·开发语言·数据结构·算法·嵌入式软件
龚礼鹏2 小时前
图像显示框架十二——BufferQueue的工作流程(基于Android 15源码分析)
android
独自破碎E2 小时前
LCR005-最大单词长度乘积
java·开发语言
2401_838472512 小时前
单元测试在C++项目中的实践
开发语言·c++·算法