线程管理:Java线程与pthread映射、线程池与调度、线程状态转换
聊到Android Runtime的线程管理,我脑子里第一个蹦出来的画面,是几年前在优化一个短视频App的卡顿问题。那时候我盯着systrace,发现主线程动不动就被卡住,后来一查,是后台线程池炸了,几百个线程在抢CPU。嗯,从那以后,我对线程管理就特别上心。
说白了,Java层的线程,最终都要落到Linux的pthread上。ART作为中间人,怎么映射、怎么调度、怎么管理状态,这里面的门道不少。今天我就把这些经验掰开揉碎,跟你聊聊。
Java线程与pthread的映射关系
你写一个new Thread().start(),ART在底层干了什么?我告诉你,它其实调用了pthread_create。但这不是简单的1:1映射,中间还有一层封装。
核心映射逻辑:
- 每个Java线程对象,对应一个Native的
Thread结构体 - 这个结构体里,保存了pthread_t、线程栈地址、线程状态等
- ART通过
pthread_create创建底层线程,然后让这个线程去执行Java的run()方法
我记得有一次,我在看ART源码里的thread.cc,发现它有个CreateNativeThread函数。这个函数会先分配一个ManagedThread对象,然后调用pthread_create。你想想看,如果pthread_create失败了,Java层会抛OutOfMemoryError,而不是直接崩溃。这个设计挺巧妙的。
// ART源码简化示意
void Thread::CreateNativeThread(JNIEnv* env, jobject java_thread, size_t stack_size) {
pthread_t new_pthread;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
// 设置栈大小,默认是1MB
if (stack_size > 0) {
pthread_attr_setstacksize(&attr, stack_size);
}
int result = pthread_create(&new_pthread, &attr, Thread::CreateCallback, java_thread);
if (result != 0) {
// 抛出OutOfMemoryError
env->ThrowNew(env->FindClass("java/lang/OutOfMemoryError"), "Could not create thread");
}
}
个人习惯:我在做性能分析时,会先确认线程总数。如果Java线程数超过200,基本可以断定pthread创建开销已经成了瓶颈。这时候就该考虑线程池了。
线程状态转换
Java线程有6种状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。但ART底层,其实对应了更多细粒度的状态。我画了一张图,帮你理清这个关系。
为什么会这样设计?我个人的理解是,ART需要更细粒度的控制。比如,Java的RUNNABLE状态,在ART底层其实分成了kRunnable和kNative。前者表示线程正在执行Java代码,后者表示正在执行Native代码。这个区分对GC很重要——GC需要知道线程是否在安全点。
我曾经踩过的坑:有一次,我在一个循环里频繁调用Thread.sleep(1),结果发现GC停顿时间暴涨。后来一查,是因为线程在TIMED_WAITING和RUNNABLE之间频繁切换,每次切换都要检查安全点,导致GC线程被拖慢。解决方案很简单——用LockSupport.parkNanos()替代sleep,减少状态切换开销。
线程池与调度策略
说到线程池,我建议你直接使用ThreadPoolExecutor,而不是Executors.newFixedThreadPool()这种快捷方式。为什么?因为快捷方式隐藏了太多细节,你没法精细控制。
| 参数 | 说明 | 我的建议 |
|---|---|---|
| corePoolSize | 核心线程数 | CPU核心数 + 1(IO密集型可以更大) |
| maximumPoolSize | 最大线程数 | 不超过200,否则pthread开销太大 |
| keepAliveTime | 空闲线程存活时间 | IO密集型设60秒,CPU密集型设10秒 |
| workQueue | 任务队列 | 用LinkedBlockingQueue,别用无界队列 |
| RejectedExecutionHandler | 拒绝策略 | 用CallerRunsPolicy,别直接抛异常 |
你想想看,如果用了Executors.newCachedThreadPool(),它的maximumPoolSize是Integer.MAX_VALUE。一旦任务量暴增,线程数会疯狂上涨,最后系统资源耗尽。我在项目里见过一次,那场面,啧啧,整个App直接ANR。
// 我常用的线程池配置
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, // corePoolSize
32, // maximumPoolSize
60L, // keepAliveTime
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(128), // 有界队列
new ThreadFactory() {
private final AtomicInteger counter = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "my-pool-thread-" + counter.incrementAndGet());
thread.setDaemon(true); // 设为守护线程,避免阻止App退出
return thread;
}
},
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝时由调用线程执行
);
避坑指南:我曾经在线上环境发现,某个线程池的任务执行时间越来越长。后来定位到,是因为任务里用了synchronized锁,导致线程被BLOCKED。线程池里的线程一旦被阻塞,其他任务就得排队。解决方案是:把同步操作改成ReentrantLock配合tryLock,超时了就放弃,避免线程长时间阻塞。
ART的线程调度机制
ART本身不直接做线程调度,它依赖Linux内核的CFS(完全公平调度器)。但ART做了一件很重要的事——线程优先级映射。
Java线程的优先级范围是1到10,Linux的nice值范围是-20到19。ART怎么映射的?我告诉你,它做了个线性映射:
- Java优先级1(MIN_PRIORITY)→ Linux nice值19(最低优先级)
- Java优先级5(NORM_PRIORITY)→ Linux nice值0
- Java优先级10(MAX_PRIORITY)→ Linux nice值-20(最高优先级)
但这里有个坑。我记得有一次,我把一个后台线程的优先级设成了Thread.MAX_PRIORITY,结果它把主线程的CPU时间抢走了,导致UI卡顿。后来我才意识到,在Android上,不要轻易改线程优先级。系统已经为不同组件分配了合适的优先级,你乱改反而会破坏调度平衡。
核心原则:
- UI线程:优先级最高,不要在上面做耗时操作
- 后台任务:用默认优先级,或者稍微降低(setPriority(Thread.MIN_PRIORITY + 1))
- 实时任务:考虑用HandlerThread,而不是自己new线程
线程生命周期管理
线程的生命周期,说白了就是创建、运行、销毁。但ART在这三个环节都做了优化。
创建优化:ART会缓存一些线程相关的对象,比如ThreadLocal的存储空间。这样新线程创建时,不需要每次都重新分配。
运行优化:ART的JIT编译器会针对热点代码生成优化版本。如果一个线程反复执行同一段逻辑,JIT会把它编译成机器码,提升执行效率。
销毁优化:线程结束时,ART会回收它的栈空间和ThreadLocal变量。但要注意,如果线程是daemon线程,JVM退出时不会等待它结束。我建议后台线程都设为daemon,避免App退出时被卡住。
// 线程生命周期管理示例
public class MyThread extends Thread {
private volatile boolean running = true;
@Override
public void run() {
// 使用标志位控制线程退出
while (running) {
try {
// 执行任务
doWork();
} catch (InterruptedException e) {
// 响应中断
Thread.currentThread().interrupt();
break;
}
}
// 清理资源
cleanup();
}
public void shutdown() {
running = false;
interrupt(); // 中断等待中的操作
}
}
注意:不要用Thread.stop()来终止线程。这个方法已经被标记为废弃,因为它会强制释放所有锁,导致数据不一致。我曾经见过一个同事用stop来结束线程,结果共享数据被破坏,整个App崩溃。正确的做法是用标志位或中断机制。
嗯,线程管理这块,说到底就是三个字:控资源。控制线程数量、控制状态转换频率、控制优先级。你只要把这三点拿捏住,线程相关的性能问题基本就能避免。
我个人习惯是在每个项目里都加一个线程监控模块,定期打印线程池的状态——活跃线程数、队列长度、拒绝次数。这样一旦出现异常,能第一时间发现。你也不妨试试。
公众号:蓝海资料掘金营,微信deep3321