Android 线程、线程池的使用(一):cpu核心数和线程数有什么关系?volatile为什么使用场景有限?同步锁底层的自动优化?

147 阅读16分钟

目录

  1. 基础概念

  2. 线程

    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)​
  • ​定义​​:线程是进程内的 ​​执行单元​​,共享进程的资源(如内存、文件句柄)。

  • ​特点​​:

    • ​轻量级​​:创建和切换线程的开销远小于进程。
    • ​共享资源​​:线程可以直接访问进程
  • ​示例​​:

    • 一个浏览器可以同时打开两个网页

图片.png

​3. CPU核心数与线程数的关系

图片.png

  • ​物理核心数​​:6个(硬件实际能力)

  • ​逻辑处理器数​​ 12个(操作系统看到的"CPU"数量)

也就是说若启动6个线程,每个线程独占1个核心,无需切换,几乎独占核心。

如果超过6个, 每个核心同一时刻仍只能执行1个线程,但需在2个线程间快速切换。 ​​切换由超线程驱动​​,硬件级快速切换(纳秒级),几乎无软件开销。。​​看似12线程并行,实际是6线程交替执行​​。

如果超过12个,就会进入性能下降的风险区,切换由操作系统驱动​​:软件级上下文切换(微秒级),需保存/恢复线程状态,开销显著。

也就是三者的区别:

图片.png

4.CPU时间片轮转机制会做些什么?

公平分配CPU时间​​,避免某个线程独占CPU,通过短时间片切换,让多任务“看似同时运行”,在有限CPU核心下服务大量线程。

但,当cpu时间片耗尽的时候,操作系统立即停止当前线程,无论其是否执行完成。将线程的寄存器值、程序计数器(PC)、栈指针等存入内存。从队列中取下一个线程,将其保存的状态加载到CPU。这也是耗性能的地方。保存/恢复寄存器、更新内核调度数据结构缓存失效新任务需要重新加载数据到缓存。

5. 那么我们肯定是运行无数的app,线程肯定也是无数的,为什么不会出现性能问题呢?

线程优先级调度​​:
前台应用(如用户正在操作的APP)的线程优先级更高,获得更多时间片。后台任务(如音乐播放、下载)的线程可能被压缩时间片。

为什么海量线程不会崩溃​​:
因为时间片极短,假设时间片为5ms,6核CPU每秒可处理 6核心 × 200次/秒 = 1200线程切换,实际系统通过队列管理和优先级调度保证关键任务响应。

并且,很多框架都会对线程进行优化,比如使用线程池。

但,了解了这些理论后,我一个app应该控制线程在多少呢?如何知道开太多的线程会消耗性能呢??

  1. 响应延迟突增​​:线程数从6增至12时,延迟从50ms降至30ms → 合理范围。线程数从24增至30时,延迟从40ms升至60ms → 需减少线程数。
  2. Android Profiler的CPU分析模块:按“Wall Clock Time”排序,找到长期占用CPU的线程,看是否可以优化。

那么如何做优化呢??后面我们会了解线程池、协程

二、线程

2.1 启动app的时候,会启动几个线程?

一个普通的 Android 应用启动时,默认会创建 ​​5~10 个线程​​,根据不同的android系统版本来决定。

典型线程分类​
  1. ​主线程(main)​

    • 唯一用户线程,负责 UI 操作。
  2. ​Binder 线程(Binder:X)​

    • 通常 2~3 个,用于进程间通信(IPC)。
  3. ​JVM 守护线程​

    • Signal CatcherHeapTaskDaemonReferenceQueueDaemonFinalizerDaemon 等。
  4. ​JIT 线程(Android 8.0+)​

    • 用于动态编译优化。
  5. ​应用自定义线程​

    • 如果应用主动创建线程或使用异步框架(如 AsyncTaskRxJava),线程数会增加。
线程名类型状态作用
​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 新启线程有几种方式

两种:

  1. Thread和Runnable。

图片.png

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() 会:

  1. 抛出 InterruptedException,​​清除中断状态​​。
  2. 需在 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 状态下调用

图片.png

(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 执行完,才能继续往下走。

图片.png 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

多线程同时修改共享数据时,会出现三大问题:

  1. 原子性破坏:比如转账时,A转出100元,B同时查询余额,可能看到中间状态(A的钱已扣,但对方未收到)。
  2. 可见性问题:线程A修改了数据,线程B可能看不到最新值(因为缓存不一致)。
  3. 有序性问题:代码的编译优化可能导致指令顺序改变,引发意外结果。

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)锁的特点

  1. ​互斥性​​:同一时刻只允许一个线程持有锁。
  2. ​可重入性​​:线程可以重复获取已持有的锁(避免死锁)。比如你的方法是递归的同步方法,那么需要可重入性,内置锁具备可重入性。
  3. ​阻塞与唤醒​​:提供 wait()/notify()Condition 实现线程间协作。
  4. ​性能优化​​: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 锁会按竞争激烈程度逐步升级:

  1. 无锁:初始状态,没有线程竞争。无任何额外操作,性能最高。
  2. 偏向锁:只有一个线程访问时,记录线程ID(避免重复加锁)。也就是,如果是同一个线程进来,直接放行。避免重复加锁的开销(单线程场景下性能最优)。
  3. 轻量级锁:少量线程竞争时,通过CAS自旋尝试获取锁。- 新访客通过 ​​自旋(CAS)​​ 检查牌子是否被撤销(类似反复看门口是否挂上“空闲”标志)。如果自旋成功(原访客很快离开),新访客进入房间;如果自旋超时(竞争激烈),升级为重量级锁。主要是优化避免线程阻塞。
  4. 重量级锁:竞争激烈时,线程进入阻塞队列(真正的互斥锁)。- 管理员放弃协调,直接让访客们去排队(线程进入阻塞队列)。每次只允许一个人进入,其他人必须等待操作系统级别的调度。避免无意义的自旋消耗 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的区别就是:

  1. synchroinized使用锁,避免共享数据竞争。
  2. ThreadLocal本身就没有共享数据,为每一个线程提供变量的副本。但ThreadLocal 对象通常被声明为 static,因此所有线程都能访问同一个 ThreadLocal 实例。然后ThreadLocal内部维护了一个map,key是线程名,value是数据副本。

图片.png

学这个的时候,我就在想,居然都没有数据共享了,那么我为什么还需要使用他呢?

我们看看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 块内