1. 多线程的重要性

在现代软件开发体系中,多线程编程是提升程序执行效率、充分利用多核 CPU 资源、实现高并发处理的关键技术。无论是服务端接口、消息队列、定时任务,还是大数据计算、分布式框架,都离不开多线程的支撑


2 多线程核心基础概念

2.1 进程与线程

进程:进程是操作系统进行资源分配与调度的基本单位,是程序运行的独立载体。每个进程拥有独立的内存空间、文件句柄、环境变量等资源,进程间相互隔离,一个进程崩溃通常不影响其他进程。

线程:线程是进程内部的执行单元,也是 CPU 调度与执行的最小单位。一个进程可包含一至多个线程,同一进程内的线程共享堆内存、方法区、文件描述符等资源,但拥有独立的程序计数器、虚拟机栈与本地方法栈。

核心区别

  • 资源:进程拥有独立资源,线程共享所属进程资源。
  • 开销:进程创建、切换、销毁开销大,线程更轻量。
  • 通信:进程间通信复杂,线程间通信便捷高效。
  • 稳定性:进程隔离性强,线程崩溃可能导致整个进程退出。

2.2 并发与并行

并发:单个 CPU 核心通过时间片轮转交替执行多个线程

并行:多个 CPU 核心同时执行多个线程

2.3 同步与异步        

同步:调用方必须等待任务执行完成才能继续后续逻辑,整个过程等待。

异步:调用方无需等待任务完成,可直接处理其他逻辑;任务执行完毕后通过通知、回调等机制返回结果,非阻塞。

2.4 多线程的优势与风险

优势

  • 提高 CPU 利用率,减少资源闲置。
  • 提升程序响应速度,耗时任务后台执行。
  • 模块化拆分,复杂任务并行处理。
  • 支持高并发服务端场景,提升系统吞吐量。

风险

  • 线程安全问题:共享数据竞争导致结果错乱。
  • 死锁:线程相互等待资源,程序永久阻塞。
  • 上下文切换开销:线程过多降低整体效率。
  • 内存溢出:线程栈占用过多内存

3 Java 创建线程的三种方式

Java 提供三种标准线程创建方式,适用于不同业务场景。

3.1 继承 Thread 类

实现步骤

  1. 自定义类继承 java.lang.Thread
  2. 重写 run() 方法,编写线程执行逻辑。
  3. 创建实例并调用 start() 启动线程。
class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

public class ThreadTest {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        t1.setName("Thread-01");
        t2.setName("Thread-02");
        t1.start();
        t2.start();
    }
}

注意:必须调用 start(),直接调用 run() 仅为普通方法执行,不开启新线程。

优缺点

  • 优点:实现简单,可直接使用 Thread 类 API。
  • 缺点:Java 单继承限制,无法继承其他类;任务与线程耦合,复用性差。

3.2 实现 Runnable 接口

解决单继承局限,是最常用的创建方式。

实现步骤

  1. 自定义类实现 Runnable 接口。
  2. 重写 run() 方法。
  3. 将任务对象传入 Thread 构造方法。
  4. 调用 start() 启动线程。
// 父类
class Animal {
    public void eat() {
        System.out.println("动物会吃饭");
    }
}

// 继承 Animal + 实现 Runnable = 既能当动物,又能当线程
class Dog extends Animal implements Runnable {

    @Override
    public void run() {
        System.out.println("小狗线程正在运行");
    }
}

public class Test {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.eat(); // 调用父类方法

        // 把 dog 变成线程
        Thread thread = new Thread(dog);
        thread.start();
    }
}

优缺点

  • 优点:避免单继承限制
  • 缺点:无返回值,无法抛出受检异常。

3.3 实现 Callable 接口 + FutureTask

支持返回值与异常抛出,适用于需要获取执行结果的场景。

实现步骤

  1. 自定义类实现 Callable<V> 接口。
  2. 重写 call() 方法。
  3. 封装为 FutureTask。                    FutureTask 是 Java 官方给你写好的 “适配器”让 Callable 能被 Thread 接收(因为它实现了 Runnable);帮你保存 Callable 的执行结果,让你能通过 get() 拿到返回值。
  4. 传入 Thread 并启动。
  5. 通过 get() 获取结果。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) sum += i;
        return sum;
    }
}

public class CallableTest {
    public static void main(String[] args) throws Exception {
        FutureTask<Integer> task = new FutureTask<>(new MyCallable());
        Thread t = new Thread(task);
        t.start();
        Integer result = task.get();
        System.out.println("执行结果:" + result);
    }
}

优缺点

  • 优点:支持返回值;可抛出受检异常。
  • 缺点:实现稍复杂;get() 会阻塞主线程。

3.4 三种方式对比

表格

方式 继承限制 返回值 异常 复用性 适用场景
继承 Thread 单继承 不支持 简单无复用任务
实现 Runnable 不支持 通用无返回任务
实现 Callable 支持 需要结果的异步任务

4 Thread 类常用 API 详解

4.1 常用构造方法

  • Thread()
  • Thread(String name)
  • Thread(Runnable target)
  • Thread(Runnable target, String name)

4.2 核心方法

  • start():启动线程,进入就绪状态。
  • run():线程执行体。
  • getName()/setName():获取 / 设置线程名。
  • currentThread():获取当前执行线程。
  • sleep(long):休眠指定毫秒,进入计时等待。
  • setPriority(int):设置优先级(1–10)。
  • setDaemon(boolean):设置为守护线程。

4.3 线程优先级

优先级仅为调度建议,不保证执行顺序:

4.4 守护线程

守护线程为用户线程提供后台服务,所有用户线程结束后,守护线程自动退出

thread.setDaemon(true);
thread.start();

5 线程的生命周期与状态转换

Java 线程共 6 种状态

  1. 新建:创建线程对象,未调用 start()
  2. 就绪:调用 start(),等待 CPU 时间片。
  3. 阻塞:等待锁释放。
  4. 等待wait()/join(),无超时。
  5. 计时等待sleep()/wait(long)
  6. 终止:执行完毕或异常退出。

6 线程安全问题与成因

6.1 什么是线程安全

多个线程同时读写共享数据时,因执行顺序不可控导致结果与预期不一致,称为线程不安全。

6.2 不安全成因

  1. 存在共享数据。
  2. 多线程同时执行修改操作。

典型场景:多线程同时对同一变量执行自增操作。


7 同步机制与锁实现

Java 提供三种线程安全解决方案:

7.1 同步代码块

synchronized(锁对象) {
    // 共享数据操作
}

锁对象可以是任意对象,同一时刻仅一个线程可持有。

7.2 同步方法

public synchronized void method() {
    // 同步逻辑
}
  • 非静态方法:锁为 this
  • 静态方法:锁为当前类 Class 对象。

同步方法 vs 同步代码块

同步方法其实就是把整个方法包成了一个同步代码块,等价于:

public void saleTicket() {
    synchronized (this) {
        // 方法体全部内容
    }
}

而同步代码块可以只锁关键代码:

public boolean saleTicket() {
    // 不操作共享数据的代码可以写在外面,不用等锁
    synchronized (this) {
        if (tickets > 0) {
            // ...
            return false;
        }
        return true;
    }
}

7.3 Lock 显式锁

ReentrantLock 是常用实现,比 synchronized 更灵活。

Lock lock = new ReentrantLock();
try {
    lock.lock();
    // 执行代码
} finally {
    lock.unlock();
}

7.4 synchronized 与 Lock 对比

  • synchronized:隐式锁,自动释放,性能高,功能简洁。
  • Lock:显式锁,手动释放,支持公平锁、超时、中断。

8 死锁的产生与避免

8.1 死锁产生的四个必要条件

  1. 互斥条件:资源同一时间仅一个线程持有。
  2. 请求与保持:持有资源并继续请求其他资源。
  3. 不可剥夺:资源只能由持有线程主动释放。
  4. 循环等待:线程形成资源等待环路。

8.2 避免死锁的常用方案

  1. 避免嵌套锁。
  2. 统一加锁顺序。
  3. 使用带超时的锁。

9 生产者消费者模型

用处: 生产消费速度不匹配、线程协作同步

基于 wait/notify/notifyAll 实现等待唤醒机制,必须在 synchronized 内部使用

  • wait():释放锁,进入等待队列。
  • notify():随机唤醒一个等待线程。
  • notifyAll():唤醒所有等待线程。

生产者消费者模型

  • 仓库满:生产者等待。
  • 仓库空:消费者等待。
// 仓库类
class Warehouse {
    private int goods = 0; // 商品数量
    private final int MAX = 5; // 仓库最大容量

    // 生产者方法:往仓库放商品
    public synchronized void produce() throws InterruptedException {
        // 仓库满了,生产者等待
        while (goods >= MAX) {
            wait(); // 释放锁,进入等待
        }
        // 没满,继续生产
        goods++;
        System.out.println(Thread.currentThread().getName() + " 生产了商品,当前库存:" + goods);
        // 叫醒消费者来消费
        notifyAll();
    }

    // 消费者方法:从仓库拿商品
    public synchronized void consume() throws InterruptedException {
        // 仓库空了,消费者等待
        while (goods <= 0) {
            wait(); // 释放锁,进入等待
        }
        // 没空,继续消费
        goods--;
        System.out.println(Thread.currentThread().getName() + " 消费了商品,当前库存:" + goods);
        // 叫醒生产者来生产
        notifyAll();
    }
}

public class Test {
    public static void main(String[] args) {
        Warehouse warehouse = new Warehouse();

        // 生产者线程
        new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    warehouse.produce();
                    Thread.sleep(100); // 模拟生产耗时
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "生产者").start();

        // 消费者线程
        new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    warehouse.consume();
                    Thread.sleep(200); // 模拟消费耗时
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "消费者").start();
    }
}

10 阻塞队列原理与使用

阻塞队列是封装好的、专门用来极简实现生产者消费者的工具

阻塞队列是线程安全的队列,内置等待唤醒逻辑,是实现生产者消费者的最优方案。

10.1 常用阻塞队列

  • ArrayBlockingQueue:数组实现,有界。
  • LinkedBlockingQueue:链表实现,可选有界。
  • PriorityBlockingQueue:优先级队列。
  • SynchronousQueue:不存储元素,直接传递。

10.2 核心方法

  • put():队列满则阻塞。
  • take():队列空则阻塞。

11 线程池原理与四大内置线程池

11.1 线程池优势

  1. 复用线程,避免频繁创建销毁开销。
  2. 控制并发线程数,防止资源耗尽。
  3. 统一管理、监控、调优。

11.2 四大内置线程池

  1. newFixedThreadPool(n)固定线程数,无界队列,适用于任务量稳定场景。

  2. newCachedThreadPool()无核心线程,可自动扩容,适用于大量短生命周期任务。

  3. newSingleThreadExecutor()单线程串行执行,保证任务顺序。

  4. newScheduledThreadPool(n)支持定时、周期执行任务。


12 自定义线程池实现

ThreadPoolExecutor 是线程池的核心实现类。

ThreadPoolExecutor pool = new ThreadPoolExecutor(
    5,                // 核心线程数
    10,               // 最大线程数
    60L,              // 空闲线程存活时间
    TimeUnit.SECONDS, // 时间单位
    new ArrayBlockingQueue<>(100), // 阻塞队列
    Executors.defaultThreadFactory(), // 线程工厂
    new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);

12.1 七大参数

  1. corePoolSize:核心线程数
  2. maximumPoolSize:最大线程数
  3. keepAliveTime:空闲时间
  4. unit:时间单位
  5. workQueue:阻塞队列
  6. threadFactory:线程工厂
  7. handler:拒绝策略


13 多线程常见面试要点总结

  1. 进程与线程的区别。
  2. 并发与并行的区别。
  3. 创建线程的三种方式及对比。
  4. start()run() 的区别。
  5. 线程的六种状态及转换。
  6. 线程安全成因及三种解决方案。
  7. synchronized 与 Lock 的区别。
  8. 死锁的四个条件与避免方案。
  9. 线程池优势、七大参数、四大内置线程池。

更多推荐