1 线程基础

1.1 进程与线程的区别

进程是操作系统进行资源分配和调度的基本单位,每个Android应用运行在一个独立的进程中,系统为其分配独立的内存空间。线程是进程内的执行单元,是CPU调度和执行的基本单位,一个进程可以包含多个线程,这些线程共享进程的内存空间和资源。

在Android系统中,理解进程与线程的区别至关重要。当应用启动时,系统会为其创建一个主线程(也称为UI线程),负责处理用户交互和界面更新。如果需要执行耗时操作,则需要创建子线程来处理,以避免阻塞主线程。

1.2 线程的生命周期与状态

Java线程在其生命周期中会经历多种状态,掌握这些状态及其转换条件对正确管理线程至关重要:

  • New(新建):线程对象被创建但尚未启动,此时start()方法还未被调用。
  • Runnable(可运行):线程已启动并正在执行或准备执行,等待CPU分配时间片。注意:在Java线程模型中,Running(运行中)和Ready(就绪)都被归为此状态。
  • Blocked(阻塞):线程试图获取一个内部对象锁(不是Java.util.concurrent锁)而该锁正被其他线程持有。
  • Waiting(等待):线程进入等待状态,需要其他线程做出特定动作(通知或中断),通过Object.wait()、Thread.join()或LockSupport.park()方法进入。
  • Timed Waiting(定时等待):与Waiting状态类似,但可以在指定时间后自动返回,通过Thread.sleep()、Object.wait(timeout)、Thread.join(timeout)等方法进入。
  • Terminated(终止):线程已执行完毕或因异常退出,不可再次启动。

表:Java线程状态转换及触发方法

状态转换 触发方法
New → Runnable start()方法被调用
Runnable → Blocked 尝试获取已被其他线程持有的对象锁
Runnable → Waiting 调用Object.wait()、Thread.join()或LockSupport.park()
Runnable → Timed Waiting 调用Thread.sleep()、Object.wait(timeout)等带超时参数的方法
阻塞/等待状态 → Runnable 相应的唤醒条件满足(如锁可用、超时时间到、被通知/中断)
Runnable → Terminated run()方法执行完成或抛出未捕获异常

1.3 线程的创建方式

在Java中,有几种创建线程的方式,每种方式各有适用场景:

  • 继承Thread类:通过扩展Thread类并重写其run()方法
  • 实现Runnable接口:实现Runnable接口并将实例作为参数传递给Thread构造函数
  • 使用Callable和Future:可以返回结果和抛出异常,配合ExecutorService使用

实现Runnable接口相比继承Thread类更具优势,因为它避免了Java单继承的限制,允许线程类继承其他类;同时更适合多个线程共享同一资源的情况,因为Runnable对象可以被多个线程共享。而Callable与Future的组合则提供了更强大的功能,允许线程返回执行结果、捕获异常,并支持取消操作。

1.4 线程安全

当多个线程同时访问共享资源时,如果不采取适当的同步措施,可能会导致数据不一致、脏读等问题,这就是线程安全问题。Android提供了多种机制来保证线程安全:

  • synchronized关键字:可以修饰方法或代码块,确保同一时刻只有一个线程能执行该段代码。它基于对象监视器锁实现,支持可重入性。
  • ReentrantLock:比synchronized更灵活的锁机制,支持公平锁、非公平锁,提供 Condition 条件变量,可以精确控制线程的等待与唤醒。
  • volatile关键字:保证变量的可见性(一个线程修改后其他线程立即可见)和禁止指令重排序,但不保证原子性。
  • 原子类:如AtomicInteger、AtomicLong等,基于CAS(Compare-And-Swap)操作实现,提供原子性的读写操作。
  • 不可变对象:创建后状态不可改变的对象,如String、Integer等,天然线程安全。

死锁是多线程编程中常见的问题,指两个或更多线程互相等待对方持有的资源,导致所有线程都无法继续执行。避免死锁的策略包括:避免嵌套锁、按固定顺序获取锁、使用尝试获取锁的机制(tryLock())以及设置超时时间。

2 Android线程模型

2.1 主线程(UI线程)与ANR

在Android应用中,主线程(也称作UI线程)是应用启动时系统自动创建的主要线程,它承担着多项关键职责:

  • 处理所有用户交互事件(点击、触摸、滚动等)
  • 绘制和更新用户界面
  • 分发事件给相应的UI组件
  • 执行Activity、Fragment等组件的生命周期回调

Android系统规定所有UI操作必须在主线程中执行,如果在子线程中直接更新UI,会抛出CalledFromWrongThreadException。

ANR(Application Not Responding) 是Android系统中重要的监控机制,当应用的主线程被阻塞时间过长时,系统会弹出ANR对话框。触发ANR的主要条件有:

  • 输入事件无响应:5秒内未能响应输入事件(如按键或触摸)
  • BroadcastReceiver超时:10秒内未能完成BroadcastReceiver的执行
  • Service无响应:20秒内前台Service未执行完毕(某些情况)

避免ANR的策略是将耗时操作(如网络请求、复杂计算、数据库读写等)移出主线程,放到后台线程中执行,待完成后通过特定机制通知主线程更新UI。

2.2 Handler机制原理

Handler机制是Android系统线程间通信的核心框架,特别是用于从子线程向主线程传递消息和任务。它由四个核心组件构成:

  • Message:需要传递的消息对象,可以携带数据、任务和目标Handler
  • MessageQueue:消息队列,按时间顺序存储待处理的Message,每个线程只有一个
  • Looper:消息循环器,不断从MessageQueue中取出消息并分发给对应的Handler
  • Handler:消息的处理者和发送者,负责发送消息和处理消息

Looper.prepare() 方法会创建当前线程的Looper实例,并存储在ThreadLocal中,确保每个线程只有一个Looper。Looper.loop() 方法启动一个无限循环,不断从MessageQueue中取出消息,然后调用msg.target.dispatchMessage(msg)将消息分发给对应的Handler处理。

主线程的Looper是在ActivityThread的main()方法中创建的,这也是为什么主线程自动具有消息循环能力的原因。

Handler的工作流程包括发送消息和处理消息两个部分。发送消息可以通过sendMessage()、sendMessageDelayed()、post(Runnable)等方法,这些方法最终都会将消息或Runnable封装成Message并放入MessageQueue。处理消息时,Handler的dispatchMessage()方法会根据情况调用handleMessage()或直接执行Runnable任务。

避免Handler内存泄漏是关键问题,因为Handler如果作为非静态内部类,会隐式持有外部类(如Activity)的引用。解决方案包括:使用静态内部类+弱引用(WeakReference),并在Activity销毁时调用handler.removeCallbacksAndMessages(null)清除所有待处理消息。

HandlerThread是Android提供的一种特殊线程,它内部已经创建了Looper和MessageQueue,可以直接用于创建Handler,简化了带有消息循环能力的后台线程的创建。

2.3 AsyncTask、IntentService及其替代方案

AsyncTask是Android早期提供的轻量级异步任务类,它封装了线程池和Handler,提供了onPreExecute()、doInBackground()、onProgressUpdate()和onPostExecute()等方法,分别在不同阶段和线程执行。但由于其存在生命周期问题、内存泄漏风险、并发控制不一致等缺点,现已被官方标记为废弃(Deprecated)。

IntentService是一种特殊的Service,它内部使用工作线程串行处理所有Intent请求,在处理完所有请求后会自动停止。但由于与Service组件本身的限制以及Android对后台执行的限制,也已被官方标记为废弃。

现代Android开发中的替代方案包括:

  • Kotlin协程:轻量级并发解决方案,通过挂起函数实现非阻塞式异步编程,结合viewModelScope和lifecycleScope提供自动的生命周期管理
  • WorkManager:用于处理可延迟的后台任务,保证任务最终会执行,适合不需要立即执行的持久化任务
  • 线程池+LiveData/ViewModel:结合Java标准线程池和Android架构组件,提供可感知生命周期的后台任务管理

3 线程池详解

3.1 为什么需要线程池

在Android开发中,直接创建线程虽然简单,但存在诸多问题,而线程池提供了专业的解决方案:

  • 资源消耗大:每个Java线程默认占用约1MB栈内存,大量线程会消耗宝贵的内存资源,可能导致OOM(内存溢出)
  • 创建销毁开销大:线程的创建和销毁需要调用系统内核函数,是昂贵的操作,频繁操作会影响性能
  • 管理困难:直接创建的线程分散在代码各处,难以统一管理、监控和取消

线程池的优势体现在以下几个方面:

  • 资源复用:通过复用已创建的线程,减少线程创建和销毁的开销
  • 控制并发数:通过参数控制并发线程数量,防止线程过多导致系统过载
  • 管理任务队列:提供任务队列缓冲突发的大量任务,平滑系统负载
  • 提供监控:通过钩子方法监控任务执行状态,便于问题排查和性能优化

3.2 线程池核心参数

ThreadPoolExecutor是Java线程池的核心实现类,其构造函数包含7个核心参数,这些参数共同决定了线程池的行为特性:

  1. corePoolSize(核心线程数):线程池中长期保持的线程数量,即使这些线程处于空闲状态也不会被销毁(除非设置allowCoreThreadTimeOut为true)
  2. maximumPoolSize(最大线程数):线程池允许创建的最大线程数量,当工作队列满时,线程池会创建新线程直到达到此限制
  3. keepAliveTime(空闲线程存活时间):当线程数量超过核心线程数时,多余的空闲线程在终止前等待新任务的最长时间
  4. unit(时间单位):keepAliveTime参数的时间单位,如TimeUnit.SECONDS、TimeUnit.MILLISECONDS等
  5. workQueue(工作队列):用于保存等待执行的任务的阻塞队列,常见的有LinkedBlockingQueue、ArrayBlockingQueue、SynchronousQueue等
  6. threadFactory(线程工厂):用于创建新线程的工厂,可以自定义线程名称、优先级、守护状态等
  7. handler(拒绝策略):当工作队列已满且线程数已达到最大值时,用于处理新提交任务的策略

表:线程池参数配置策略

参数 CPU密集型任务 IO密集型任务 混合型任务
corePoolSize CPU核数 + 1 2* CPU核数 根据任务比例调整
maximumPoolSize corePoolSize + 1 或相同 corePoolSize* 2 适中扩展
workQueue 有界队列(大小适中) 有界或无界队列 有界队列
keepAliveTime 较短时间(如30-60秒) 较长时间(如1-2分钟) 适中时间

3.3 线程池工作机制

线程池的任务处理遵循一套精心设计的流程,如下图所示:

具体来说,当有新任务提交时,线程池的处理逻辑如下:

  1. 如果当前线程数小于核心线程数,线程池会创建新的核心线程执行任务
  2. 如果当前线程数达到或超过核心线程数,任务会被放入工作队列等待执行
  3. 如果工作队列已满,且当前线程数小于最大线程数,线程池会创建新的非核心线程执行任务
  4. 如果工作队列已满,且当前线程数达到最大线程数,新任务会被拒绝执行,根据拒绝策略处理

3.4 常见的线程池类型

Executors类提供了一系列工厂方法用于创建常见配置的线程池,但在生产环境中需要谨慎使用,因为某些配置可能存在资源耗尽的风险:

  • FixedThreadPool:固定大小的线程池,使用无界队列LinkedBlockingQueue,适用于负载较重的服务器,需要限制线程数量的场景
  • CachedThreadPool:可根据需要创建新线程的线程池,使用SynchronousQueue,最大线程数为Integer.MAX_VALUE,适用于执行很多短期异步任务的小程序或负载较轻的服务器
  • SingleThreadExecutor:单线程线程池,使用无界队列,保证所有任务按顺序执行,适用于需要顺序执行任务的场景
  • ScheduledThreadPool:支持定时及周期性任务执行的线程池,适用于需要多个后台线程执行周期任务的场景

阿里Java开发规约不建议直接使用Executors创建线程池,因为FixedThreadPool和SingleThreadExecutor使用的无界队列可能导致内存溢出,而CachedThreadPool的最大线程数设置过大可能导致线程数量失控。推荐的方式是直接通过ThreadPoolExecutor构造函数创建线程池,这样可以明确指定所有参数,避免潜在风险。

4 线程池源码解析

4.1 ThreadPoolExecutor执行流程

ThreadPoolExecutor是Java线程池的核心实现,其设计精巧且高效。理解其源码对深入掌握线程池工作机制至关重要。

ctl变量是ThreadPoolExecutor的核心控制状态,它是一个AtomicInteger类型的原子变量,巧妙地将线程池状态和线程数量封装在一个int值中:

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 高3位存储线程池状态,低29位存储线程数量
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

// 线程池状态
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

线程池状态变迁如下:RUNNING → SHUTDOWN → STOP → TIDYING → TERMINATED

execute方法是ThreadPoolExecutor的核心方法,负责处理任务提交:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
  
    int c = ctl.get();
    // 阶段1:当前线程数小于核心线程数
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 阶段2:线程池处于RUNNING状态且任务成功入队
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // 再次检查状态,如果非RUNNING则移除任务并执行拒绝策略
        if (!isRunning(recheck) && remove(command))
            reject(command);
        // 如果线程数为0,则创建非核心线程(保证任务能被处理)
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 阶段3:队列已满,尝试创建非核心线程
    else if (!addWorker(command, false))
        // 创建失败(超过最大线程数),执行拒绝策略
        reject(command);
}

这个方法清晰地体现了线程池的三层处理逻辑:先尝试创建核心线程,然后尝试入队,最后尝试创建非核心线程,如果都失败则执行拒绝策略。

4.2 Worker机制

ThreadPoolExecutor中的工作线程被封装在Worker内部类中,它继承自AbstractQueuedSynchronizer并实现了Runnable接口:

private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
    final Thread thread; // 真正执行任务的线程
    Runnable firstTask;  // 初始任务,可能为null
  
    Worker(Runnable firstTask) {
        setState(-1); // 禁止中断直到runWorker
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }
  
    public void run() {
        runWorker(this); // 委托给外部类的runWorker方法
    }
    // ... 其他方法省略
}

Worker使用AQS实现了一个简单的不可重入锁,主要用于中断控制。state=0表示锁未被占用,state=1表示锁已被占用。

runWorker方法是工作线程执行的核心循环:

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // 允许中断
    boolean completedAbruptly = true;
    try {
        // 循环获取任务:首先执行firstTask,然后从队列中获取
        while (task != null || (task = getTask()) != null) {
            w.lock();
            // 如果线程池正在停止,确保线程被中断
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                beforeExecute(wt, task); // 钩子方法
                Throwable thrown = null;
                try {
                    task.run(); // 执行任务
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    afterExecute(task, thrown); // 钩子方法
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        // 处理工作线程退出
        processWorkerExit(w, completedAbruptly);
    }
}

runWorker方法通过while循环不断从任务队列中获取任务并执行,直到getTask()返回null,此时工作线程会正常退出。

getTask方法负责从工作队列中获取任务,它实现了线程池的keepAliveTime机制:

private Runnable getTask() {
    boolean timedOut = false; // 上次poll是否超时
  
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);
    
        // 检查线程池状态和队列状态
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }
    
        int wc = workerCountOf(c);
    
        // 判断是否允许超时退出:允许核心线程超时 或 当前线程数大于核心线程数
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
    
        // 检查是否需要减少工作线程数
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }
    
        try {
            // 根据timed选择poll(带超时)或take(阻塞)
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true; // 获取任务超时
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

getTask方法通过timed变量控制工作线程的退出策略。当timed为true时,使用poll方法等待keepAliveTime时间,超时后返回null导致工作线程退出;当timed为false时,使用take方法无限期等待,保证核心线程常驻(除非设置allowCoreThreadTimeOut为true)。

文章作者: 嘿手大叔
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 i·Space
学习记录 Java 线程 线程池
喜欢就支持一下吧
打赏
微信 微信
支付宝 支付宝