在 Java 技术面试中,多线程知识始终是考察的重点领域,它不仅反映候选人的基础功底,更能体现对并发编程的理解深度。对于程序员而言,掌握多线程基础知识不仅是通过面试的必备技能,更是编写高效、安全并发程序的基础。本文将以程序员视角,系统梳理多线程核心概念、实现方式及常见问题解决方案,通过代码示例解析面试中的高频考点,帮助读者构建完整的多线程知识体系。

多线程核心概念解析:从理论到内存模型

理解多线程的核心概念是掌握并发编程的基础,这些概念不仅是面试中的高频问题,更是解决实际并发问题的理论依据。从线程的本质到 Java 内存模型的抽象,每个概念都对应着并发编程中的具体场景和问题。

线程的生命周期与状态转换是多线程编程的基础理论,直接影响线程的创建、执行和销毁过程:


public class ThreadLifecycleDemo {

public static void main(String[] args) throws InterruptedException {

// 新建状态:线程对象已创建但未启动

Thread thread = new Thread(() -> {

try {

// 运行状态:执行run()方法

System.out.println("线程执行中 - 运行状态");

// 超时等待:调用sleep()方法

Thread.sleep(1000);

// 等待状态:调用wait()方法(需要同步锁)

synchronized (ThreadLifecycleDemo.class) {

ThreadLifecycleDemo.class.wait();

}

} catch (InterruptedException e) {

e.printStackTrace();

}

}, "生命周期演示线程");

// 获取线程状态:新建状态

System.out.println("线程创建后状态:" + thread.getState());

// 启动线程:进入就绪状态

thread.start();

Thread.sleep(100); // 等待线程启动

System.out.println("线程启动后状态:" + thread.getState());

// 等待线程进入睡眠状态

Thread.sleep(200);

System.out.println("线程睡眠时状态:" + thread.getState());

// 等待线程进入等待状态

Thread.sleep(1200);

System.out.println("线程等待时状态:" + thread.getState());

// 唤醒等待中的线程

synchronized (ThreadLifecycleDemo.class) {

ThreadLifecycleDemo.class.notify();

}

// 等待线程执行完毕

thread.join();

System.out.println("线程结束后状态:" + thread.getState());

}

}

这段代码演示了线程的完整生命周期及状态转换:

  • 新建状态(New):线程对象创建后尚未启动的状态
  • 就绪状态(Runnable):调用 start () 方法后等待 CPU 调度的状态
  • 运行状态(Running):CPU 调度后执行 run () 方法的状态
  • 阻塞 / 等待状态(Blocked/Waiting):因竞争锁或调用 wait () 方法进入的状态
  • 超时等待状态(Timed Waiting):调用 sleep (time) 等带超时参数方法的状态
  • 终止状态(Terminated):线程执行完毕或异常终止的状态

Java 内存模型(JMM) 定义了线程和主内存之间的抽象关系,是理解线程安全的关键:


public class JmmDemo {

// 共享变量:未加volatile关键字

private static boolean flag = false;

public static void main(String[] args) throws InterruptedException {

// 线程1:读取flag值

Thread readerThread = new Thread(() -> {

System.out.println("读取线程启动,开始等待flag变为true");

while (!flag) {

// 循环等待flag变化

}

System.out.println("读取线程检测到flag变为true,退出循环");

}, "读取线程");

// 启动读取线程

readerThread.start();

Thread.sleep(1000); // 确保读取线程先启动

// 线程2:修改flag值

Thread writerThread = new Thread(() -> {

System.out.println("写入线程启动,设置flag为true");

flag = true;

System.out.println("写入线程已设置flag为true");

}, "写入线程");

// 启动写入线程

writerThread.start();

}

}

上述代码在未给flag变量添加volatile关键字时,可能出现读取线程永远无法退出循环的情况,这是因为:

  • 线程 2 修改的 flag 值可能仅保存在工作内存中,未及时刷新到主内存
  • 线程 1 始终从自己的工作内存读取 flag 值,无法感知主内存中的变化
  • 这种可见性问题可通过添加volatile关键字解决,确保变量修改的可见性

理解 JMM 需要掌握三个核心特性:

  • 原子性:操作不可分割,要么全部执行要么不执行
  • 可见性:一个线程的修改能被其他线程及时看到
  • 有序性:程序执行顺序按照代码顺序执行(避免指令重排序影响)

线程实现方式与核心 API:从创建到协作

Java 提供了多种线程实现方式和丰富的 API 支持,掌握这些实现方式的特点和适用场景,以及核心 API 的正确用法,是编写多线程程序的基础技能,也是面试中的常见考点。

线程的多种实现方式各有特点,适用于不同的业务场景:


// 方式1:继承Thread类

class ExtendThread extends Thread {

@Override

public void run() {

System.out.println("继承Thread类实现线程 - 当前线程:" + Thread.currentThread().getName());

// 线程执行逻辑

}

}

// 方式2:实现Runnable接口

class RunnableImpl implements Runnable {

@Override

public void run() {

System.out.println("实现Runnable接口实现线程 - 当前线程:" + Thread.currentThread().getName());

// 线程执行逻辑

}

}

// 方式3:实现Callable接口(有返回值)

class CallableImpl implements Callable<Integer> {

@Override

public Integer call() throws Exception {

System.out.println("实现Callable接口实现线程 - 当前线程:" + Thread.currentThread().getName());

// 线程执行逻辑,计算结果

int result = 0;

for (int i = 0; i <= 100; i++) {

result += i;

}

return result; // 返回计算结果

}

}

public class ThreadImplementationDemo {

public static void main(String[] args) throws ExecutionException, InterruptedException {

// 方式1:启动继承Thread的线程

new ExtendThread().start();

// 方式2:启动实现Runnable的线程

new Thread(new RunnableImpl()).start();

// 方式3:启动实现Callable的线程(通过FutureTask获取结果)

FutureTask<Integer> futureTask = new FutureTask<>(new CallableImpl());

new Thread(futureTask).start();

// 获取线程执行结果

Integer result = futureTask.get();

System.out.println("Callable线程计算结果:1到100的和为" + result);

// 方式4:使用线程池(推荐方式)

ExecutorService executorService = Executors.newFixedThreadPool(3);

// 提交Runnable任务

executorService.submit(new RunnableImpl());

// 提交Callable任务

Future<Integer> future = executorService.submit(new CallableImpl());

System.out.println("线程池执行结果:" + future.get());

// 关闭线程池

executorService.shutdown();

}

}

三种基础实现方式的对比分析:

  • 继承 Thread 类:实现简单,但无法继承其他类,缺乏灵活性
  • 实现 Runnable 接口:避免单继承限制,可共享资源,但无返回值
  • 实现 Callable 接口:支持返回值和异常抛出,配合 FutureTask 使用
  • 线程池方式:是实际开发中的推荐方式,可有效管理线程资源

线程协作的核心 API用于实现线程间的同步与通信:


public class ThreadCoordinationDemo {

// 共享资源

private static final Queue<Integer> queue = new LinkedList<>();

private static final int MAX_SIZE = 5; // 队列最大容量

public static void main(String[] args) {

// 生产者线程:向队列添加数据

Thread producer = new Thread(() -> {

for (int i = 1; i <= 10; i++) {

synchronized (queue) {

// 队列满时等待

while (queue.size() == MAX_SIZE) {

try {

System.out.println("队列已满,生产者等待...");

queue.wait(); // 释放锁并等待

} catch (InterruptedException e) {

e.printStackTrace();

}

}

// 生产数据

queue.offer(i);

System.out.println("生产者生产:" + i + ",当前队列大小:" + queue.size());

queue.notifyAll(); // 通知消费者

try {

Thread.sleep(100); // 模拟生产耗时

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}, "生产者线程");

// 消费者线程:从队列获取数据

Thread consumer = new Thread(() -> {

for (int i = 1; i <= 10; i++) {

synchronized (queue) {

// 队列空时等待

while (queue.isEmpty()) {

try {

System.out.println("队列已空,消费者等待...");

queue.wait(); // 释放锁并等待

} catch (InterruptedException e) {

e.printStackTrace();

}

}

// 消费数据

int data = queue.poll();

System.out.println("消费者消费:" + data + ",当前队列大小:" + queue.size());

queue.notifyAll(); // 通知生产者

try {

Thread.sleep(150); // 模拟消费耗时

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}, "消费者线程");

// 启动线程

producer.start();

consumer.start();

}

}

线程协作中常用的核心方法:

  • wait():释放对象锁,进入等待状态,需在同步块中使用
  • notify()/notifyAll():唤醒等待该对象锁的线程,需在同步块中使用
  • join():等待调用该方法的线程执行完毕
  • sleep(long millis):让线程休眠指定时间,不释放锁
  • yield():暂停当前线程,让渡 CPU 资源给其他线程

并发问题与解决方案:从线程安全到性能优化

多线程编程在带来效率提升的同时,也引入了线程安全问题。理解常见的并发问题产生原因,掌握相应的解决方案,是编写可靠并发程序的关键,也是面试中的重点考察内容。

线程安全问题的产生与解决需要从根源上理解竞争条件和共享资源访问问题:


// 线程不安全的计数器示例

class UnsafeCounter {

private int count = 0;

// 非线程安全的自增方法

public void increment() {

count++; // 实际包含三个操作:读取-修改-写入

}

public int getCount() {

return count;

}

}

// 线程安全的计数器实现

class SafeCounter {

// 方式1:使用synchronized关键字

private int syncCount = 0;

public synchronized void syncIncrement() {

syncCount++;

}

public synchronized int getSyncCount() {

return syncCount;

}

// 方式2:使用原子类(更高效)

private final AtomicInteger atomicCount = new AtomicInteger(0);

public void atomicIncrement() {

atomicCount.incrementAndGet();

}

public int getAtomicCount() {

return atomicCount.get();

}

}

public class ThreadSafetyDemo {

public static void main(String[] args) throws InterruptedException {

// 测试线程不安全计数器

UnsafeCounter unsafeCounter = new UnsafeCounter();

// 启动多个线程同时操作

ExecutorService executor = Executors.newFixedThreadPool(10);

for (int i = 0; i < 1000; i++) {

executor.submit(unsafeCounter::increment);

}

executor.shutdown();

executor.awaitTermination(1, TimeUnit.SECONDS);

System.out.println("非线程安全计数器结果:" + unsafeCounter.getCount()); // 结果可能小于1000

// 测试synchronized实现的线程安全计数器

SafeCounter safeCounter = new SafeCounter();

ExecutorService syncExecutor = Executors.newFixedThreadPool(10);

for (int i = 0; i < 1000; i++) {

syncExecutor.submit(safeCounter::syncIncrement);

}

syncExecutor.shutdown();

syncExecutor.awaitTermination(1, TimeUnit.SECONDS);

System.out.println("Synchronized计数器结果:" + safeCounter.getSyncCount()); // 结果为1000

// 测试原子类实现的线程安全计数器

ExecutorService atomicExecutor = Executors.newFixedThreadPool(10);

for (int i = 0; i < 1000; i++) {

atomicExecutor.submit(safeCounter::atomicIncrement);

}

atomicExecutor.shutdown();

atomicExecutor.awaitTermination(1, TimeUnit.SECONDS);

System.out.println("原子类计数器结果:" + safeCounter.getAtomicCount()); // 结果为1000

}

}

线程安全问题的解决方案主要有:

  • synchronized 关键字:通过互斥锁实现同步,可修饰方法或代码块
  • volatile 关键字:保证变量的可见性,禁止指令重排序,但不保证原子性
  • 原子类:如 AtomicInteger、AtomicLong 等,提供高效的原子操作
  • 显式锁:如 ReentrantLock,提供比 synchronized 更灵活的锁定机制

线程池的合理使用与参数调优直接影响并发程序的性能:


public class ThreadPoolDemo {

public static void main(String[] args) {

// 自定义线程池(推荐方式)

ThreadPoolExecutor executor = new ThreadPoolExecutor(

2, // corePoolSize:核心线程数

5, // maximumPoolSize:最大线程数

60, // keepAliveTime:非核心线程空闲存活时间

TimeUnit.SECONDS, // 时间单位

new LinkedBlockingQueue<>(10), // 工作队列

Executors.defaultThreadFactory(), // 线程工厂

new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略

);

// 提交任务

for (int i = 0; i < 15; i++) {

int taskNum = i;

executor.submit(() -> {

try {

System.out.println("任务" + taskNum + "执行中,线程:" +

Thread.currentThread().getName());

Thread.sleep(1000); // 模拟任务执行时间

} catch (InterruptedException e) {

e.printStackTrace();

}

return taskNum;

});

}

// 关闭线程池

executor.shutdown();

}

}

线程池核心参数的理解与调优:

  • 核心线程数(corePoolSize):始终保持的线程数量
  • 最大线程数(maximumPoolSize):线程池能创建的最大线程数量
  • 工作队列:用于保存等待执行的任务,不同队列类型影响线程池行为
  • 拒绝策略:当任务数超过最大处理能力时的处理方式
    • AbortPolicy:直接抛出异常(默认)
    • CallerRunsPolicy:由提交任务的线程执行
    • DiscardPolicy:直接丢弃任务
    • DiscardOldestPolicy:丢弃队列中最旧的任务

合理配置线程池需要考虑:

  • CPU 密集型任务:线程数一般设置为 CPU 核心数 + 1
  • I/O 密集型任务:线程数可设置为 CPU 核心数 ×2 或更高
  • 任务优先级和执行时间:不同类型任务可使用不同线程池

掌握多线程基础知识不仅是应对面试的需要,更是编写高效、安全并发程序的基础。从核心概念到实际应用,从问题分析到解决方案,构建完整的知识体系才能在面试中从容应对,在实际开发中得心应手。理解线程生命周期、Java 内存模型、线程实现方式、同步机制和线程池原理,将为深入学习并发编程打下坚实基础,也是程序员技术成长的重要一步。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐