Android 线程、线程池的使用(三):如何去查看线程池是一个合理的状态?线程池的各种队列;线程池的参数;并发编程;

336 阅读15分钟

目录

  1. 阻塞队列
  2. 线程池
  3. 周期性线程池
  4. 如何去查看一个线程池是一个合理的状态去优化线程池。

一、阻塞队列

1. 基本概念
  • 队列(Queue) :一种 先进先出(FIFO) 的数据结构,元素从队尾添加(入队),从队首移除(出队)。
  • 阻塞(Blocking) :当线程尝试操作队列时,若条件不满足(如队列为空或满),线程会被 挂起(阻塞) ,直到条件满足才唤醒。
2. 阻塞队列是什么?
  • 定义:一种支持阻塞操作的 线程安全队列,提供以下特性:
    • 队列为空时:消费者线程尝试取数据会被阻塞,直到队列非空。
    • 队列已满时:生产者线程尝试存数据会被阻塞,直到队列有空位。
  • 核心接口:Java 中的 BlockingQueue 接口。
3. 为什么线程池中使用阻塞队列,而不是其他的呢?
  1. 协调速率:当队列满时,生产者线程被阻塞,避免无限制提交任务导致 OOM;当队列空时,消费者线程被阻塞,避免无意义轮询浪费 CPU。
  2. 线程安全设计​​:阻塞队列内部已实现同步机制(如 ReentrantLock + Condition),无需手动加锁。

普通队列的(非线程安全)需自行处理线程同步和阻塞逻辑,复杂易出错 。

4.常用的阻塞队列
​场景​​推荐队列​​理由​
内存敏感型任务(如低端设备)ArrayBlockingQueue有界队列避免内存溢出,严格限制任务堆积。
通用任务缓冲(默认推荐)LinkedBlockingQueue无界队列简化管理,适合任务量可控的场景。
高吞吐量、短任务SynchronousQueue直接传递任务,避免缓冲开销,最大化线程利用率。
按优先级处理任务PriorityBlockingQueue支持自定义优先级,适合需要分级处理的场景(如 VIP 用户请求优先)。
定时或延迟任务DelayQueue按延迟时间调度任务,适合心跳检测、倒计时等场景。

有界:队列长度有限制,满了以后,就会阻塞,添加不了。

无界:队列长度没限制,添加不会阻塞,但是,当队列为空时,消费者线程会被阻塞。

接下来,我们先看看线程池,然后在看看阻塞队列和线程池的一起使用。

5.举例使用

BlockingQueue queue = new ArrayBlockingQueue<>(10); // 有界队列,容量10
// 生产者线程
new Thread(() -> {
    try {
        for (int i = 0; i < 20; i++) {
            queue.put(i); // 队列满时阻塞
            System.out.println("duilie 生产: " + i);
            Thread.sleep(100);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}).start();

// 消费者线程
new Thread(() -> {
    try {
        while (true) {
            Object value = queue.take(); // 队列空时阻塞
            System.out.println("duilie 消费: " + value);
            Thread.sleep(2000);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}).start();

图片.png


二、线程池

线程池是一种线程管理机制,通过预先创建一组线程并复用它们来执行多个任务,避免频繁创建和销毁线程的开销。其核心目标是 提升系统资源利用率降低任务执行延迟

2.1 目的

  1. 缩短任务总执行时间
    • 减少线程创建/销毁开销:线程的创建和销毁涉及系统调用和资源分配,复用线程可大幅减少这类开销。
    • 降低任务调度延迟:任务到达时,线程池中已有可用线程,无需等待新线程创建。
  1. 线程是稀缺而昂贵的资源
    • 内存占用:每个线程需分配栈内存(默认1MB),线程过多易导致内存耗尽。
    • 上下文切换开销:线程数超过CPU核心数时,频繁切换线程会降低CPU利用率。
    • 系统限制:操作系统对线程数有上限(如Linux默认为1024),超出会导致错误。

2.2 线程池如何使用?

我们直接来个例子:

// 创建自定义线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
        2,  // 核心线程数
        4,  // 最大线程数
        60, // 空闲线程存活时间(秒)
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(10), // 任务队列容量10
        new ThreadPoolExecutor.AbortPolicy() // 拒绝策略:抛异常
);

try {
    // 提交15个任务(测试拒绝策略)
    for (int i = 0; i < 15; i++) {
        final int taskId = i;
        executor.execute(() -> {
            try {
                Log.d(TAG, "onCreate: "+"完成任务: " + taskId + ",线程: " + Thread.currentThread().getName());
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
} finally {
    executor.shutdown();
}

执行流程:

  1. 当调用 executor.execute() 提交任务时,任务0和任务1会立即执行,因为我们的核心线程数是2,所以两个任务立马得到执行,而不是放到队列里面在进行阻塞。
  2. 当核心线程已满,任务会进入队列等待,比如2~11任务就会进入队列阻塞,那么这个时候队列就满了。
  3. 此时,如果核心线程还在处理任务,并且队列也满了,然后你还在提交任务,那么就会触发创建临时线程(最大线程数4 - 核心线程2 = 2个临时线程),那么此时,就会有4个线程在运行。
  4. 如果说已满足线程数最大值4,队列已满10个任务,如果还添加任务,那么就会根据我们设置的拒绝策略,就会抛出异常。
  5. 核心线程thread-1thread-2会复用。临时线程thread-3thread-4在处理完任务后,60秒内可能被复用

2.3 核心组件:

  1. ThreadPoolExecutor:最常用的线程池实现,支持精细参数配置:
  • corePoolSize:核心线程数(常驻线程),线程池长期保留的线程数量,即使处于空闲状态。
  • maximumPoolSize:最大线程数(临时线程)
  • keepAliveTime:空闲线程存活时间
  • workQueue:任务队列(如LinkedBlockingQueue
  • RejectedExecutionHandler:拒绝策略
  1. 往线程池里面提交任务有两种方法:
  • execute() 提交不需要返回值的Runnable任务。
  • submit() 提交需要返回值的Callable任务。
  1. 关闭线程池的两种方式​

1. shutdown()

  • 停止接收新任务。

  • 等待已提交的任务(包括队列中的任务)执行完成。

  • ​不强制中断正在运行的任务​​。

  • ​适用场景​​:希望所有已提交任务正常完成后再关闭。

2. shutdownNow()

  • 立即停止接收新任务。

  • ​尝试中断所有正在执行的任务​​(通过Thread.interrupt())。

  • 清空任务队列,返回未执行的任务列表。

  • ​是否真正中断取决于任务是否响应中断​​。

  • ​适用场景​​:需要尽快释放资源,允许部分任务未完成。

executor.execute(() -> {
    while (true) {
        // 未检查中断状态,即使调用shutdownNow()也无法停止
        System.out.println("无法停止的任务");
    }
});

需要改写成如下这种

executor.execute(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        try {
            // 模拟工作(每次循环检查中断)
            TimeUnit.MILLISECONDS.sleep(100);
            System.out.println("工作中...");
        } catch (InterruptedException e) {
            // 捕获中断异常后,需再次设置中断标志(或退出循环)
            Thread.currentThread().interrupt(); // 重新设置中断标志
            System.out.println("任务被中断");
            break;
        }
    }
});

2.4 如何合理配置线程池?

比如我们一个app,可能有很多地方使用到了线程,比如AFragment、BFragment,我们应该如何创建线程池呢?

避免为每个模块单独创建线程池(防止资源浪费)全App共享1~3个全局线程池(如CPU池、IO池)

  • ​线程数计算公式​

    • ​CPU密集型​​:线程数 = CPU核心数 + 1
      (减少上下文切换,最大化利用CPU)
    • ​IO密集型​​:线程数 = CPU核心数 * 2
      (更多线程应对阻塞等待)
① CPU密集型任务
  • 特征
    • 线程几乎不阻塞(无网络/文件操作)。
  • 典型场景
    • 视频转码:大量数学运算压缩视频。
    • 科学计算:如物理模拟、数据加密。
    • 图像处理:滤镜渲染、3D建模。
  • 代码示例
// 计算斐波那契数列(纯CPU计算)
public class CpuTask {
    public static long fibonacci(int n) {
        if (n <= 1) return n;
        return fibonacci(n-1) + fibonacci(n-2);
    }
}
② IO密集型任务
  • 特征
    • CPU 使用率低(通常低于50%)。
    • 线程频繁阻塞(等待网络、磁盘、数据库响应)。
    • 任务执行时间长(秒级或更长)。
  • 典型场景
    • Web服务器:处理 HTTP 请求,等待数据库返回结果。
    • 文件上传/下载:网络带宽或磁盘速度是瓶颈。
    • 消息队列消费:等待消息到达或处理外部API调用。
  • 代码示例
// 从数据库查询用户信息(IO等待)
public class IoTask {
    public User getUserById(int id) {
        return jdbcTemplate.queryForObject("SELECT * FROM users WHERE id = ?", User.class, id);
    }
}
③ 混合型任务
  • 特征
    • CPU 和 IO 交替占用:先计算再等待IO,或反过来。
    • 资源使用波动大:CPU 和 IO 使用率均有峰值。
  • 典型场景
    • 数据处理流水线:读取文件(IO)→ 清洗数据(CPU)→ 写入数据库(IO)。
    • 实时推荐系统:计算用户偏好(CPU)→ 调用推荐算法服务(网络IO)。
    • 游戏服务器:物理引擎计算(CPU)→ 保存玩家状态到磁盘(IO)。
  • 代码示例
// 处理订单:验证数据(CPU)→ 调用支付接口(IO)→ 生成报表(CPU)
public class MixedTask {
    public void processOrder(Order order) {
        validate(order);                 // CPU计算
        callPaymentGateway(order);       // 网络IO
        generateReport(order);           // CPU计算
    }
}

2.5如何创建CPU、IO或者混合密集型的线程池呢?

线程池其实都一样的,只不过配置的参数调用的方法不一样。

一、CPU密集型线程池​

​适用场景​​:数学计算、图像处理、数据加密等需要大量CPU运算的任务。
​配置要点​​:

  1. 线程数 ≈ CPU核心数 + 1
  2. 使用​​有界队列​​防止资源耗尽
  3. 拒绝策略建议AbortPolicy(直接拒绝新任务)
public class CpuThreadPool {
    // 获取CPU核心数(Android设备通常4~8核)
    private static final int CPU_CORES = Runtime.getRuntime().availableProcessors();

    // CPU密集型线程池
    private static final ThreadPoolExecutor cpuExecutor = new ThreadPoolExecutor(
        CPU_CORES + 1,          // 核心线程数
        CPU_CORES + 1,          // 最大线程数(与核心线程数相同)
        30L, TimeUnit.SECONDS,  // 非核心线程空闲存活时间(此处无效)
        new ArrayBlockingQueue<>(100),  // 有界队列,容量100
        new CustomThreadFactory("CPU-Pool"), // 自定义线程工厂
        new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
    );

    // 获取线程池实例
    public static ThreadPoolExecutor getExecutor() {
        return cpuExecutor;
    }

  
}

CPU密集型任务为什么要用有界队列?​​执行时间稳定,几乎不会阻塞。如果使用无界队列(如LinkedBlockingQueue无参构造),当任务提交速度 > 处理速度时,队列会无限增长。使用有界队列(如ArrayBlockingQueue(100)),当队列满时触发拒绝策略,保护系统稳定性。


​二、IO密集型线程池​

​适用场景​​:网络请求、文件读写、数据库操作等存在阻塞等待的任务。
​配置要点​​:

  1. 线程数 ≈ CPU核心数 * 2
  2. 使用​​无界队列​​或​​同步队列​
  3. 拒绝策略建议CallerRunsPolicy(由调用线程执行)
public class IoThreadPool {
    private static final int CPU_CORES = Runtime.getRuntime().availableProcessors();

    // IO密集型线程池
    private static final ThreadPoolExecutor ioExecutor = new ThreadPoolExecutor(
        CPU_CORES * 2,          // 核心线程数
        CPU_CORES * 2,          // 最大线程数
        60L, TimeUnit.SECONDS,  // 空闲线程存活时间
        new LinkedBlockingQueue<>(),    // 无界队列(默认容量Integer.MAX_VALUE)
        new CustomThreadFactory("IO-Pool"),
        new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
    );

    public static ThreadPoolExecutor getExecutor() {
        return ioExecutor;
    }
}

IO密集型任务为什么用无界队列?​​大部分时间在等待IO响应,线程实际占用CPU时间短。使用无界队列允许更多任务排队,利用线程等待IO的空闲时间处理其他任务。


​三、混合型线程池​

​适用场景​​:任务中同时包含CPU计算和IO操作(如先下载文件再解析)。
​配置要点​​:

  1. 核心线程数按CPU密集型设置
  2. 最大线程数适当扩大
  3. 使用​​优先级队列​​区分任务重要性
public class HybridThreadPool {
    private static final int CPU_CORES = Runtime.getRuntime().availableProcessors();

    // 混合型线程池(带任务优先级)
    private static final ThreadPoolExecutor hybridExecutor = new ThreadPoolExecutor(
        CPU_CORES + 1,          // 核心线程数
        CPU_CORES * 2,          // 最大线程数
        45L, TimeUnit.SECONDS,
        new PriorityBlockingQueue<>(100), // 优先级队列
        new CustomThreadFactory("Hybrid-Pool"),
        new ThreadPoolExecutor.DiscardOldestPolicy()
    );
}

​四、线程池类型选择策略​
​场景判断方法​​选择类型​​配置示例​
任务中90%时间在计算CPU密集型线程数=CPU+1,队列容量100
任务中60%时间在等待网络响应IO密集型线程数=CPU*2,无界队列
计算与IO时间接近(如40% CPU计算)混合型核心线程=CPU+1,最大线程=CPU*2,优先级队列
​线程池类型​​核心线程数建议​​典型模块​
​网络请求池​CPU核心数 * 2Retrofit/OkHttp异步请求
​图像处理池​CPU核心数+1Bitmap解码、滤镜处理
​设备CPU核心数​​总核心线程数范围​​典型分配方案​
4核12 ~ 20CPU池(5) + IO池(8) + BG池(2) = 15
8核24 ~ 40CPU池(9) + IO池(16) + BG池(4) = 29

三、案例:ScheduledThreadPoolExecutor

Android 智能家居开发,串口读写数据,会涉及到频繁的数据读写,那么我们应该使用IO密集型,还是CPU呢?

  • 数据发送:将字节流写入串口缓冲区(由硬件处理实际传输)写入缓冲区后由硬件处理,无需持续占用CPU

  • 数据接收:轮询或中断方式读取缓冲区(可能有阻塞等待)线程可能在read()时挂起等待数据,类似网络IO等待

建议: 优先选择IO密集型线程池​​,次选混合型(因为也有可能有数据处理(如CRC校验)若校验逻辑复杂(如加密解密),则需要)。

我们读数据,一般是间隔个几秒或者几十秒,就需要读取一次数据,这里我们需要是又有定时执行的线程池,比如ScheduledThreadPoolExecutor

3.1 是什么?

ScheduledThreadPoolExecutor 是 Java 并发包中专门为​​定时任务​​和​​周期性任务​​设计的线程池,解决了以下传统方案的痛点:

​传统方案​​痛点​
Timer单线程执行任务,一个任务阻塞会导致所有后续任务延迟
Thread.sleep()需手动管理线程生命周期,无法复用线程资源
普通线程池+循环提交无法精确控制任务触发时间,代码复杂度高

3.2 如何使用

(1)创建实例

// 创建核心线程数为3的定时线程池
ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(3);

// (关闭时清理未执行任务)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    scheduled.setRemoveOnCancelPolicy(true);
}
scheduler.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
​(2)固定频率任务​
// 初始延迟0秒,之后每10秒执行一次(无视任务耗时)
scheduler.scheduleAtFixedRate(() -> {
    System.out.println("固定频率任务开始: " + System.currentTimeMillis());
    try {
        Thread.sleep(3000); // 模拟任务耗时3秒
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}, 0, 10, TimeUnit.SECONDS);
​(3)固定间隔任务​
// 每次任务结束后间隔10秒再执行下一次
scheduler.scheduleWithFixedDelay(() -> {
    System.out.println("固定间隔任务开始: " + System.currentTimeMillis());
    try {
        Thread.sleep(3000); // 模拟任务耗时3秒
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}, 0, 10, TimeUnit.SECONDS);
  • 不要用scheduleAtFixedRate执行可能超时的任务

  • 推荐使用scheduleWithFixedDelay处理不确定耗时的任务

​(4)关闭线程池​
// 平缓关闭(等待正在执行的任务完成)
scheduler.shutdown();

// 强制关闭(立即终止所有任务)
if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) {
    scheduler.shutdownNow();
}

shutdown:仅允许已开始的周期任务完成当前周期,不会中断,允许任务完成,但会丢弃未到期的延迟任务。而 ThreadPoolExecutor的shutdown是会执行完队列中​​所有任务​​。

shutdownNow:适用于​​紧急终止​​场景,强制中断所有任务并清空队列,允许任务未完成,但需任务代码配合响应中断。

ScheduledThreadPoolExecutor 默认使用的是 AbortPolicy 拒绝策略。具体行为如下:

  1. ​默认策略​​:
    继承自 ThreadPoolExecutor 的默认拒绝策略 AbortPolicy,当任务无法被接受时抛出 RejectedExecutionException

  2. ​触发条件​​:
    由于 ScheduledThreadPoolExecutor 使用无界的 DelayedWorkQueue(队列容量实际为 Integer.MAX_VALUE),在正常情况下队列不会满,因此拒绝策略​​仅在以下场景触发​​:

    • 线程池已关闭(shutdownshutdownNow)时提交新任务。
    • 系统资源耗尽(如内存不足导致无法创建新线程或入队任务)。

四、如何去查看线程池是一个合理的状态

通过以下关键指标判断线程池是否健康,并识别潜在问题:

​指标​​获取方法​​健康状态参考值​
​活跃线程数​executor.getActiveCount()长期接近最大线程数 → 可能需扩容
​队列大小​executor.getQueue().size()持续超过队列容量80% → 需调整队列容量或拒绝策略
​已完成任务数​executor.getCompletedTaskCount()与提交任务数对比,差值过大 → 可能存在任务堆积或拒绝
​拒绝任务数​自定义计数器(如AtomicLong rejectedTasks拒绝次数 > 0 → 需优化任务提交频率或调整线程池容量
​任务平均执行时间​任务内记录时间差 → 统计平均值执行时间远大于预期 → 需优化任务逻辑或拆分任务
​最大线程存活时间​executor.getKeepAliveTime(TimeUnit)临时线程存活时间应与任务波动周期匹配
4.1、监控方法与工具​
​1. 内置API监控(代码级)​
// 定时打印线程池状态(每30秒)
ScheduledExecutorService monitor = Executors.newSingleThreadScheduledExecutor();
monitor.scheduleAtFixedRate(() -> {
    System.out.println(
        "活跃线程: " + executor.getActiveCount() +
        " 队列大小: " + executor.getQueue().size() +
        " 完成数: " + executor.getCompletedTaskCount() +
        " 拒绝数: " + rejectedTasks.get()
    );
}, 0, 30, TimeUnit.SECONDS); // 每30秒检查一次

​2. 参数配置评估指标​
​参数​​不合理表现​​优化方向​
​corePoolSize​活跃线程数长期 < corePoolSize → 资源浪费减少核心线程数
​maximumPoolSize​频繁创建临时线程 → 线程数常达最大值增大maximumPoolSize 或优化任务逻辑
​workQueue​队列持续满载 → 任务响应延迟增大队列容量 或 使用有界队列+合理拒绝策略
​rejectedPolicy​大量任务被拒绝 → 业务数据丢失改用CallerRunsPolicy 或 增加降级逻辑
​2. 任务类型与参数调整​
​任务类型​​推荐配置​
​CPU密集型​- 核心线程数 = CPU核心数 + 1 - 使用有界队列(如ArrayBlockingQueue
​IO密集型​- 核心线程数 = CPU核心数 * 2 - 使用无界队列(如LinkedBlockingQueue
​混合型任务​- 拆分任务到不同线程池 - 核心线程数按主要任务类型配置
​3. Android Profiler(可视化工具)​​【AI分享】

通过 Android Studio 的 ​​Profiler​​ 实时分析线程状态:

  1. ​CPU Profiler​​:

    • 查看线程池工作线程的CPU占用率
    • 定位高耗时任务(火焰图)
  2. ​Memory Profiler​​:

    • 检测线程泄漏(线程数量持续增长)
  3. ​Network Profiler​​:

    • 结合网络请求分析IO密集型线程池的负载

​4. 第三方库辅助​

  • ​BlockCanary​​:检测UI卡顿,定位线程池任务阻塞主线程

  •   implementation 'com.github.markzhai:blockcanary-android:1.5.0'
    
  • ​LeakCanary​​:检测线程泄漏

  •      debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'