目录
-
基础概念
-
线程
2.1 启动app的时候,会启动几个线程?
2.2 新启线程有几种方式
2.3 如何安全的停止工作呢?
interrupt
方法2.4 线程的一些方法
2.5 synchroinized
2.6 volatile
4.7 ThreadLocal,是什么,为什么使用?
4.8 线程协作是什么?
一、基础概念
1. 进程(Process)
-
定义:进程是操作系统 资源分配的基本单位。每个进程拥有独立的:
- 内存空间(代码、数据、堆栈)
- 文件句柄、网络连接等资源。
-
特点:
- 隔离性:进程之间相互隔离,一个进程崩溃不会直接影响其他进程。
- 开销大:创建和销毁进程需要分配或回收资源(如内存),成本较高。
-
示例:
- 同时运行的 Chrome 浏览器和微信是两个不同的进程。
2. 线程(Thread)
-
定义:线程是进程内的 执行单元,共享进程的资源(如内存、文件句柄)。
-
特点:
- 轻量级:创建和切换线程的开销远小于进程。
- 共享资源:线程可以直接访问进程
-
示例:
- 一个浏览器可以同时打开两个网页
3. CPU核心数与线程数的关系
-
物理核心数:6个(硬件实际能力)
-
逻辑处理器数 12个(操作系统看到的"CPU"数量)
也就是说若启动6个线程,每个线程独占1个核心,无需切换,几乎独占核心。
如果超过6个, 每个核心同一时刻仍只能执行1个线程,但需在2个线程间快速切换。 切换由超线程驱动,硬件级快速切换(纳秒级),几乎无软件开销。。看似12线程并行,实际是6线程交替执行。
如果超过12个,就会进入性能下降的风险区,切换由操作系统驱动:软件级上下文切换(微秒级),需保存/恢复线程状态,开销显著。
也就是三者的区别:
4.CPU时间片轮转机制会做些什么?
公平分配CPU时间,避免某个线程独占CPU,通过短时间片切换,让多任务“看似同时运行”,在有限CPU核心下服务大量线程。
但,当cpu时间片耗尽的时候,操作系统立即停止当前线程,无论其是否执行完成。将线程的寄存器值、程序计数器(PC)、栈指针等存入内存。从队列中取下一个线程,将其保存的状态加载到CPU。这也是耗性能的地方。保存/恢复寄存器、更新内核调度数据结构。缓存失效新任务需要重新加载数据到缓存。
5. 那么我们肯定是运行无数的app,线程肯定也是无数的,为什么不会出现性能问题呢?
线程优先级调度:
前台应用(如用户正在操作的APP)的线程优先级更高,获得更多时间片。后台任务(如音乐播放、下载)的线程可能被压缩时间片。
为什么海量线程不会崩溃:
因为时间片极短,假设时间片为5ms,6核CPU每秒可处理 6核心 × 200次/秒 = 1200线程切换
,实际系统通过队列管理和优先级调度保证关键任务响应。
并且,很多框架都会对线程进行优化,比如使用线程池。
但,了解了这些理论后,我一个app应该控制线程在多少呢?如何知道开太多的线程会消耗性能呢??
-
响应延迟突增:线程数从6增至12时,延迟从50ms降至30ms → 合理范围。线程数从24增至30时,延迟从40ms升至60ms → 需减少线程数。
- Android Profiler的CPU分析模块:按“Wall Clock Time”排序,找到长期占用CPU的线程,看是否可以优化。
那么如何做优化呢??后面我们会了解线程池、协程。
二、线程
2.1 启动app的时候,会启动几个线程?
一个普通的 Android 应用启动时,默认会创建 5~10 个线程,根据不同的android系统版本来决定。
典型线程分类
-
主线程(main)
- 唯一用户线程,负责 UI 操作。
-
Binder 线程(Binder:X)
- 通常 2~3 个,用于进程间通信(IPC)。
-
JVM 守护线程
Signal Catcher
、HeapTaskDaemon
、ReferenceQueueDaemon
、FinalizerDaemon
等。
-
JIT 线程(Android 8.0+)
- 用于动态编译优化。
-
应用自定义线程
- 如果应用主动创建线程或使用异步框架(如
AsyncTask
、RxJava
),线程数会增加。
- 如果应用主动创建线程或使用异步框架(如
线程名 | 类型 | 状态 | 作用 |
---|---|---|---|
main | 用户线程 | RUNNABLE | 主线程(UI线程),负责处理界面渲染、用户输入和事件分发。 |
Signal Catcher | 守护线程 | WAITING | 捕获和处理 JVM 信号(如 SIGQUIT ),用于调试或崩溃日志生成。 |
Jit thread pool worker 0 | 守护线程 | RUNNABLE | JIT(即时编译器)线程,负责将字节码动态编译为机器码,优化应用性能。 |
Binder:3036_1 | 用户线程 | RUNNABLE | Binder IPC 线程,用于进程间通信(如与系统服务或其他进程交互)。 |
HeapTaskDaemon | 守护线程 | WAITING | 堆内存管理线程,执行垃圾回收相关的后台任务(如内存压缩、对象分配跟踪)。 |
Binder:3036_2 | 用户线程 | RUNNABLE | 另一个 Binder IPC 线程,通常用于处理多个并发 IPC 请求。 |
ReferenceQueueDaemon | 守护线程 | WAITING | 引用队列守护线程,处理软引用、弱引用等被回收后的清理工作。 |
Profile Saver | 守护线程 | RUNNABLE | 性能分析线程,记录应用运行时的性能数据(如方法热点),用于优化。 |
FinalizerDaemon | 守护线程 | WAITING | 终结器线程,调用对象的 finalize() 方法(已废弃,但部分遗留代码可能依赖)。 |
FinalizerWatchdogDaemon | 守护线程 | TIMED_WAITING | 终结器监控线程,确保 finalize() 方法不会长时间阻塞,防止内存泄漏。 |
2.2 新启线程有几种方式
两种:
- Thread和Runnable。
Thread是对线程的抽象。Runnable不是一个线程,是对任务,业务逻辑的抽象。任务可以被多个线程执行。
2.3 如何安全的停止工作呢?interrupt
方法
interrupt()
是 Java 线程的一个方法,用于向线程发送一个中断信号。它不会直接终止线程,而是设置线程的中断状态(一个布尔标志),线程需要自行检查该状态并决定如何响应。
- 协作式终止:线程可以选择在安全点停止,避免资源泄漏或数据不一致。
class WorkerThread extends Thread {
@Override
public void run() {
while (!isInterrupted()) {//自动结束
try {
doTask(); // 执行任务
} catch (InterruptedException e) {
// 收到中断异常,退出
Thread.currentThread().interrupt();
}
}
cleanup(); // 安全释放资源
}
private void doTask() throws InterruptedException {
// 模拟可能阻塞的操作
Thread.sleep(500);
}
private void cleanup() {
// 释放数据库连接等资源
}
}
// 终止线程
WorkerThread thread = new WorkerThread();
thread.start();
// 发送中断请求
thread.interrupt();
当线程处于 sleep()
, wait()
, join()
等阻塞状态时,调用 interrupt()
会:
- 抛出
InterruptedException
,清除中断状态。 - 需在
catch
块中决定终止或恢复状态:
很多时候,我们会看到,使用一个boolean变量来作为线程的中断,这样是不可取的,因为如果线程处于 sleep()
, wait()
, I/O
或锁竞争(如 synchronized
)等阻塞状态,即使 boolean
标志被设置为 true
,线程也无法立即退出,必须等待阻塞结束后才能检查标志。
Thread thread = new Thread(() -> {
try {
while (true) {
System.out.println("运行中...");
Thread.sleep(1000);
}
} finally {
System.out.println("这段代码可能不会执行!");
}
});
thread.start();
TimeUnit.SECONDS.sleep(2);
thread.stop(); // 立即终止线程
但是如果你使用interrupt方法的话,就会抛出 InterruptedException
异常来将线程中断。
2.4 线程的一些方法
(1)run和start方法
run()
:只是一个普通方法,直接调用 run()
不会创建新线程。
start()
:是触发新线程启动的入口,会异步执行 run()
方法,并由 JVM 调度新线程运行。
Java 线程的生命周期中,每个 Thread
对象只能经历一次从 NEW(新建)到 RUNNABLE(可运行)的状态转换。
调用 start()
方法时,JVM 会检查线程的底层状态(通过 threadStatus
字段),若线程已启动(状态非 NEW),则抛出异常。所以重复调用 start()
方法会抛出 IllegalThreadStateException
,start()
只能在 NEW 状态下调用。
(2)join方法
让别人插队 。比如让别人打饭,他会等别人打完饭,才到他。 我们可以使用这个方法,让线程顺序执行。
public class Test {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println("线程1:煮米饭");
});
Thread t2 = new Thread(() -> {
System.out.println("线程2:炒菜");
});
Thread t3 = new Thread(() -> {
System.out.println("线程3:摆碗筷");
});
// 按顺序执行:煮饭 → 炒菜 → 摆碗筷
t1.start();
t1.join(); // 主线程卡住,直到t1结束
t2.start();
t2.join(); // 主线程卡住,直到t2结束
t3.start();
t3.join();
System.out.println("开饭啦!");
}
}
谁调用 join()
,谁就要等待 , 在 main
方法中调用 t1.join()
,表示主线程要等 t1
执行完,才能继续往下走。
join()
内部是通过 ·wait()
实现的,调用时会释放锁,当线程终止时,会调用notifyAll唤醒等待的线程。
注意,是JVM 会自动调用该线程对象的 notifyAll()
,唤醒所有因调用该线程的 join()
而等待的线程。
这里的 notifyAll()
并非由 Java 代码显式调用,而是 JVM 在底层线程终止逻辑中隐式触发的。具体实现位于 JVM 的 C++ 源码中。
(3)线程优先级
public class ThreadPriorityDemo {
public static void main(String[] args) {
Runnable task = () -> {
System.out.println(Thread.currentThread().getName() + " 优先级: " +
Thread.currentThread().getPriority());
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + " 执行第 " + i + " 次");
}
};
Thread t1 = new Thread(task, "线程-低优先级");
Thread t2 = new Thread(task, "线程-高优先级");
// 设置优先级(范围:1~10,默认5)
t1.setPriority(Thread.MIN_PRIORITY); // 1
t2.setPriority(Thread.MAX_PRIORITY); // 10
t1.start();
t2.start();
}
}
优先级默认是5,1~10,10最高,在启动前要设置优先级。
JVM 的线程调度器会参考优先级分配 CPU 时间片,但不保证高优先级线程一定先执行(依赖操作系统实现)。只能说,聊胜于无。
(4)守护线程
GC也是守护线程。
用户线程=非守护线程
所有守护线程会随着用户线程结束而结束。
执行流程是:先用户线程结束,然后守护线程结束。
public class DaemonThreadDemo {
public static void main(String[] args) {
Thread daemonThread = new Thread(() -> {
while (true) { // 无限循环
try {
System.out.println("守护线程运行中...");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "守护线程");
Thread userThread = new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("用户线程执行第 " + i + " 次");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "用户线程");
// 设置为守护线程(必须在 start() 前调用)
daemonThread.setDaemon(true);
daemonThread.start();
userThread.start();
// 主线程等待用户线程结束
try {
userThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("用户线程已结束,程序终止");
}
}
注意事项
- 不要用守护线程处理关键任务:比如写入文件、更新数据库,可能因突然终止导致数据不完整。
当所有用户线程(包括主线程)结束时,守护线程会自动终止。但 Android 主线程的特殊性意味着,守护线程在 Android 中通常不会自动终止(除非应用进程被销毁),了解即可。
2.5 synchroinized
多线程同时修改共享数据时,会出现三大问题:
- 原子性破坏:比如转账时,A转出100元,B同时查询余额,可能看到中间状态(A的钱已扣,但对方未收到)。
- 可见性问题:线程A修改了数据,线程B可能看不到最新值(因为缓存不一致)。
- 有序性问题:代码的编译优化可能导致指令顺序改变,引发意外结果。
synchronized
能解决这三个问题!它通过锁机制,保证代码块的原子性、可见性和有序性。
synchronized
是用于实现线程同步的关键字,它的核心作用是 解决多线程并发访问共享资源时的数据竞争(race condition)问题,确保线程安全。
synchronized
通过 加锁机制 实现线程同步,保证同一时刻最多只有一个线程能执行被同步的代码块或方法。
它基于 对象监视器(Monitor) 实现,每个 Java 对象都有一个内置锁(也称为监视器锁),线程需先获取锁才能执行同步代码。
(1)锁
同步代码块:显式指定锁对象。
synchronized (lockObject) {
// 临界区代码(需互斥执行的代码)
}
同步方法:隐式使用当前对象(实例方法)或类对象(静态方法)作为锁。
//锁:当前类的实例对象
public synchronized void method() { ... }
//锁:类锁,为一个class对象
public static synchronized void staticMethod() { ... }
类锁的锁对象是类的 Class
对象。每个类被 JVM 加载时,都会在内存中生成一个唯一的 Class
对象(可通过 类名.class
或 对象.getClass()
获取)。
(2)锁的特点
- 互斥性:同一时刻只允许一个线程持有锁。
- 可重入性:线程可以重复获取已持有的锁(避免死锁)。比如你的方法是递归的同步方法,那么需要可重入性,内置锁具备可重入性。
- 阻塞与唤醒:提供
wait()
/notify()
或Condition
实现线程间协作。 - 性能优化:JVM 对锁进行分级(偏向锁、轻量级锁、重量级锁)以减少开销。
public class Counter {
public synchronized void add(int n) {
if (n <= 0) return;
System.out.println("Add: " + n);
add(n - 1); // 递归调用,需要可重入性
}
}
// 线程调用:new Counter().add(3);
(3)锁的自动优化
为了提高性能,synchronized
锁会按竞争激烈程度逐步升级:
- 无锁:初始状态,没有线程竞争。无任何额外操作,性能最高。
- 偏向锁:只有一个线程访问时,记录线程ID(避免重复加锁)。也就是,如果是同一个线程进来,直接放行。避免重复加锁的开销(单线程场景下性能最优)。
- 轻量级锁:少量线程竞争时,通过CAS自旋尝试获取锁。- 新访客通过 自旋(CAS) 检查牌子是否被撤销(类似反复看门口是否挂上“空闲”标志)。如果自旋成功(原访客很快离开),新访客进入房间;如果自旋超时(竞争激烈),升级为重量级锁。主要是优化避免线程阻塞。
- 重量级锁:竞争激烈时,线程进入阻塞队列(真正的互斥锁)。- 管理员放弃协调,直接让访客们去排队(线程进入阻塞队列)。每次只允许一个人进入,其他人必须等待操作系统级别的调度。避免无意义的自旋消耗 CPU。
这个是JVM 根据实际运行情况动态调整,无需开发者干预。
2.6 volatile
volatile
是Java中用于修饰变量的关键字,主要解决 可见性 和 指令重排序 问题,但不保证原子性。它是比 synchronized
更轻量的同步机制,但适用于特定场景。
为什么使用呢?
- 问题一:普通变量的修改可能仅存在于线程的本地缓存(如CPU缓存),其他线程无法立即看到。
- 解决:
volatile
变量的修改会直接写入主内存,其他线程读取时强制从主内存刷新。 - 问题二:JVM或处理器可能对指令进行重排序优化,导致多线程下出现意外结果。
- 解决:
volatile
通过内存屏障(Memory Barrier)禁止编译器和处理器重排序。
适用场景有限
- 适用:一写多读、状态标志位等简单同步场景。直接赋值(原子操作:
flag = true
)
// 正确示例:volatile 修饰状态标志位
public class Server {
private volatile boolean isRunning = true;
public void shutdown() {
isRunning = false; // 单一线程修改(原子操作)
}
public void serve() {
while (isRunning) { // 多个线程读取
// 处理请求
}
}
}
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 防止指令重排序
}
}
}
return instance;
}
}
- 不适用:复合操作(
count++
、value = x + y
)、多线程更新共享变量
2.7 ThreadLocal,是什么,为什么使用?
也是一种避免多线程共享数据时竞争的工具。他的原理是为每个线程创建独立的变量副本,实现线程间的数据隔离。每个线程通过 ThreadLocal
访问自己的变量副本。
synchroinized和ThreadLocal的区别就是:
- synchroinized使用锁,避免共享数据竞争。
- ThreadLocal本身就没有共享数据,为每一个线程提供变量的副本。但
ThreadLocal
对象通常被声明为static
,因此所有线程都能访问同一个ThreadLocal
实例。然后ThreadLocal
内部维护了一个map,key是线程名,value是数据副本。
学这个的时候,我就在想,居然都没有数据共享了,那么我为什么还需要使用他呢?
我们看看handle中就使用了ThreadLocal,Android 的 Looper
类内部使用 ThreadLocal
确保每个线程只有一个 Looper
实例。例如,主线程的 Looper
用于更新 UI。后台线程的 Looper
负责处理耗时任务。
所以ThreadLocal
确保每个线程通过 Looper.myLooper()
获取到的是 自己线程的 Looper
实例,而非其他线程的实例。
2.8 线程协作
什么叫线程协作呢?我们以生产者和消费者为例,比如我生成一个饼出来,我会通知消费者来买,而不是让消费者一直来问饼有没有做好。你问过我一次还没做好,就等我通知就行,这个过程就是叫协作。
那么线程里是如何写作的呢?借助 wait()
和 notify()
方法,他是object的方法。
class bingtai {
private int bing = 0;
// 生产者制作饼
public synchronized void 制作汉堡() throws InterruptedException {
while (bing == 5) {
wait(); // 柜台满了,生产者休息
}
bing++;
System.out.println("生产饼,当前数量:" + bing);
notifyAll(); // 叫醒消费者
}
// 消费者买饼
public synchronized void buyBing() throws InterruptedException {
while (bing == 0) {
wait(); // 柜台空了,消费者等待
}
bing--;
System.out.println("消费饼,剩余数量:" + bing);
notifyAll(); // 叫醒生产者
}
}
介绍一下这两个方法:
wait()
:线程主动休息,让出锁。notifyAll()
:叫醒所有等待的线程(类似喊一声“汉堡做好了!”)。被唤醒后,他会重新竞争锁,而不是立马就获得锁。这里还有一个点需要注意,wait会立马释放锁,而notifyAll则不会,而是等syn执行完之后。- wait() / notify() / notifyAll()- - 必须在
synchronized
代码块内调用。
2.9 总结一下,不同的休眠方法,是否会释放锁?
方法 | 锁行为 | 作用 | 调用位置 |
---|---|---|---|
Thread.yield() | 不释放锁 | 让出 CPU,优化调度 | 任意位置 |
Thread.sleep() | 不释放锁 | 让出 CPU,线程休眠指定时间 | 任意位置 |
Object.wait() | 释放锁 | 让出 CPU,线程等待,直到被唤醒 | synchronized 块内 |
Object.notify() | 不释放锁(需退出同步块后释放) | 唤醒一个等待线程 | synchronized 块内 |
Object.notifyAll() | 不释放锁(需退出同步块后释放) | 唤醒所有等待线程 | synchronized 块内 |