南京市网站建设_网站建设公司_企业官网_seo优化
2026/1/17 17:56:53 网站建设 项目流程

在Java开发中,多线程与并发是核心知识点,也是面试中的高频考点。本文将针对四个关键问题展开详细分析,结合原理和实例帮大家理清思路,夯实基础。

一、run()和start()方法的区别

在创建并启动线程时,我们常接触到run()和start()两个方法,二者看似相似,实则底层逻辑和作用完全不同,核心区别在于是否启动新线程。

1. 方法作用与底层原理

  • start()方法:属于Thread类的方法,其核心作用是启动一个新线程。调用start()后,JVM会为该线程分配CPU资源、栈空间等,将线程状态从NEW转为RUNNABLE,待CPU调度时会自动执行run()方法。需要注意的是,start()方法不能被重复调用,否则会抛出IllegalThreadStateException异常。

  • run()方法:属于Runnable接口的抽象方法,Thread类实现了该方法,其作用是定义线程要执行的任务逻辑。run()方法本质上就是一个普通方法,直接调用时不会启动新线程,只会在当前线程中同步执行其内部逻辑,无法实现多线程并发。

2. 实例演示

public class ThreadDemo { public static void main(String[] args) { Thread thread = new Thread(() -> { System.out.println("当前线程:" + Thread.currentThread().getName()); }, "子线程"); // 1. 调用start():启动新线程,输出子线程名称 thread.start(); // 2. 直接调用run():在主线程中执行,输出main // thread.run(); } }

输出结果(调用start()时):当前线程:子线程;直接调用run()时,输出:当前线程:main

3. 核心总结

start()负责“启动线程”,是多线程的入口;run()负责“执行任务”,是线程的任务载体。只有调用start(),才能真正实现多线程并发,直接调用run()仅为普通方法调用。

二、任务和线程的区别

任务(Task)和线程(Thread)是多线程编程中的两个核心概念,二者是“任务内容”与“执行载体”的关系,很多开发者容易混淆,需明确二者的边界。

1. 概念定义

  • 任务(Task):本质是一段待执行的逻辑代码,是“要做什么”。在Java中,任务通常通过Runnable接口(无返回值)、Callable接口(有返回值)来定义,仅封装了执行逻辑,不具备执行能力。

  • 线程(Thread):是操作系统调度的基本单位,是“谁来做”。线程负责承载任务,获取CPU资源后执行任务中的逻辑,一个线程可以执行一个或多个任务(需手动调度),多个线程也可以并发执行多个任务。

2. 关系类比与实例

类比:任务相当于“快递包裹”,线程相当于“快递员”。快递员(线程)负责承载包裹(任务)并完成配送(执行逻辑),一个快递员可以送多个包裹,多个快递员也可以同时送不同包裹。

// 定义任务(Runnable实现) class MyTask implements Runnable { private String taskName; public MyTask(String taskName) { this.taskName = taskName; } @Override public void run() { System.out.println(Thread.currentThread().getName() + "执行任务:" + taskName); } } public class TaskThreadDemo { public static void main(String[] args) { // 1. 创建两个任务 MyTask task1 = new MyTask("任务1"); MyTask task2 = new MyTask("任务2"); // 2. 创建一个线程,执行两个任务(手动调度) Thread thread = new Thread(() -> { task1.run(); task2.run(); }, "线程A"); thread.start(); // 3. 创建两个线程,分别执行两个任务(并发执行) Thread threadB = new Thread(task1, "线程B"); Thread threadC = new Thread(task2, "线程C"); threadB.start(); threadC.start(); } }

3. 核心总结

任务是逻辑载体,线程是执行载体;线程依赖任务存在(无任务的线程无意义),任务可独立于线程存在(可被多个线程复用)。在实际开发中,我们常通过线程池将任务分配给线程执行,实现任务与线程的解耦。

三、wait()/notify()中if与while判断的区别(虚假唤醒问题)

在多线程通讯中,wait()和notify()/notifyAll()用于线程间的协作(如生产者-消费者模式),而判断线程唤醒后是否执行逻辑时,使用if判断会出现“虚假唤醒”,while循环则可避免,核心原因与线程唤醒机制和循环校验有关。

1. 虚假唤醒的定义

虚假唤醒(Spurious Wakeup)指线程在没有被其他线程调用notify()/notifyAll()唤醒的情况下,意外从wait()状态被唤醒。这种情况可能由操作系统的线程调度机制、JVM底层实现等因素导致(虽然概率较低,但必须处理)。

2. if判断与while循环的差异分析

  • if判断(存在虚假唤醒风险):if是一次性判断,线程被唤醒后会直接跳出判断语句,执行后续逻辑,不会再次校验唤醒条件是否满足。若发生虚假唤醒,线程会在条件不满足的情况下执行逻辑,导致业务异常。

  • while循环(避免虚假唤醒):while是循环判断,线程被唤醒后会再次校验条件是否满足。若为虚假唤醒(条件不满足),会重新调用wait()进入等待状态;只有当条件满足时,才会跳出循环执行后续逻辑,从根源上规避虚假唤醒。

3. 实例演示(生产者-消费者模式)

// 仓库类(存在线程通讯) class Warehouse { private int count = 0; private final int MAX = 10; // 仓库最大容量 // 生产(生产者线程调用) public synchronized void produce() throws InterruptedException { // 错误:使用if判断,存在虚假唤醒 // if (count >= MAX) { // 正确:使用while循环,避免虚假唤醒 while (count >= MAX) { System.out.println("仓库已满,生产者等待"); wait(); // 生产者等待 } count++; System.out.println("生产者生产,当前库存:" + count); notifyAll(); // 唤醒消费者 } // 消费(消费者线程调用) public synchronized void consume() throws InterruptedException { // 错误:使用if判断 // if (count <= 0) { // 正确:使用while循环 while (count <= 0) { System.out.println("仓库为空,消费者等待"); wait(); // 消费者等待 } count--; System.out.println("消费者消费,当前库存:" + count); notifyAll(); // 唤醒生产者 } } // 生产者线程 class Producer implements Runnable { private Warehouse warehouse; // 构造器省略... @Override public void run() { for (int i = 0; i < 20; i++) { try { warehouse.produce(); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }

4. 核心总结

在使用wait()/notify()进行线程通讯时,必须使用while循环判断唤醒条件,而非if判断。这是因为while循环能确保线程唤醒后重新校验条件,即使发生虚假唤醒,也会重新进入等待状态,保证业务逻辑的正确性。

四、wait(timeout)和sleep(timeout)的区别及后续执行逻辑

wait(timeout)和Thread.sleep(timeout)都能让线程暂停执行指定时间,但二者的底层原理、锁资源处理、唤醒机制均有差异,后续执行逻辑也不同,是面试中的高频区分题。

1. 核心区别(表格对比)

对比维度

wait(timeout)

Thread.sleep(timeout)

所属类/接口

Object类(所有对象都有该方法)

Thread类(静态方法)

锁资源处理

必须在synchronized代码块/方法中调用,调用后会释放持有的锁

无需持有锁,调用后不释放任何锁(仅暂停当前线程)

唤醒机制

1. 超时自动唤醒;2. 被其他线程调用notify()/notifyAll()唤醒;3. 被中断唤醒

1. 超时自动唤醒;2. 被中断唤醒(无法通过notify()唤醒)

使用场景

多线程通讯(需配合锁和notify())

简单的线程暂停(如延迟执行)

2. 后续执行逻辑

  • wait(timeout)后续逻辑

    • 线程调用wait(timeout)后,释放锁,进入WAITING(无超时)或TIMED_WAITING(有超时)状态;

    • 若超时未被唤醒:时间到达后,线程自动从TIMED_WAITING状态转为RUNNABLE,等待CPU调度,调度后继续执行wait()之后的代码;

    • 若超时前被notify()/notifyAll()唤醒:线程从TIMED_WAITING状态转为RUNNABLE,等待CPU调度,调度后重新竞争锁,获取锁成功后执行wait()之后的代码;

    • 若被中断唤醒:抛出InterruptedException异常,异常处理后继续执行后续代码(需手动捕获异常)。

  • Thread.sleep(timeout)后续逻辑

    • 线程调用sleep(timeout)后,不释放锁,直接进入TIMED_WAITING状态;

    • 若超时未被中断:时间到达后,线程自动转为RUNNABLE,等待CPU调度,调度后继续执行sleep()之后的代码;

    • 若超时前被中断:抛出InterruptedException异常,异常处理后继续执行后续代码;

    • 注意:sleep()期间,线程不会响应notify()/notifyAll()(即使被调用,也不会提前唤醒)。

3. 实例演示

public class WaitSleepDemo { private static final Object lock = new Object(); public static void main(String[] args) { // 演示wait(timeout) new Thread(() -> { synchronized (lock) { try { System.out.println("线程1:调用wait(2000)"); lock.wait(2000); // 等待2秒,释放锁 System.out.println("线程1:wait结束,继续执行"); } catch (InterruptedException e) { e.printStackTrace(); } } }, "线程1").start(); // 演示sleep(timeout) new Thread(() -> { synchronized (lock) { try { System.out.println("线程2:调用sleep(2000)"); Thread.sleep(2000); // 等待2秒,不释放锁 System.out.println("线程2:sleep结束,继续执行"); } catch (InterruptedException e) { e.printStackTrace(); } } }, "线程2").start(); } }

输出逻辑:线程1和线程2竞争锁,假设线程1先获取锁,调用wait(2000)释放锁,线程2获取锁调用sleep(2000)(不释放锁),2秒后线程2 sleep结束执行后续代码,释放锁,线程1 wait超时后获取锁执行后续代码。

五、总结

本文围绕多线程与并发的四个核心问题展开,梳理了run()/start()、任务/线程的本质区别,解析了虚假唤醒的规避方案,对比了wait(timeout)与sleep(timeout)的差异及执行逻辑。这些知识点是多线程开发的基础,也是面试中的重点,建议结合实例反复练习,加深理解。

后续会继续分享多线程进阶知识点(如线程池、volatile、CAS等),欢迎关注交流!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询