面试必问!Java线程控制6大核心方法(start/run区别+实操案例,代码可直接跑)

31 阅读9分钟

面试必问!Java线程控制6大核心方法(start/run区别+实操案例,代码可直接跑)

Java多线程开发中,“创建线程”只是入门,“控制线程执行节奏”才是核心!很多新手踩坑在混用start()和run()、线程执行无序、不会优雅中断线程,这些不仅是开发高频问题,更是面试必考点!今天带大家吃透线程控制的6大核心方法,结合IO、集合实战演练,代码可直接复制运行,还会揭秘面试常问的区别与避坑指南~

GZH【咖啡 Java 研习班】 面试题库和多线程学习路线图,技术交流群随时答疑!

前言:为什么线程控制这么重要?

多线程的核心价值是“并发有序执行”——如果不会控制线程的休眠、插队、优先级,程序只会变成“混乱的并发”:比如主线程提前汇总未统计完的数据、线程无法优雅停止导致资源泄漏。本文6大方法+5个实操案例,帮你精准把控线程执行节奏,彻底摆脱“线程失控”的困境!

一、避坑第一站:start()和run()的本质区别(面试必考!)

这是多线程最基础也最易踩的坑!很多人以为两者都能启动线程,实则天差地别,面试必问“为什么不能直接调用run()”。

核心区别(记牢这2点,面试不慌)

  • start()方法:真正启动新线程!调用后线程进入就绪状态,等待CPU调度,后续会自动执行run()方法。一个线程只能调用1次start(),重复调用会抛出IllegalThreadStateException(受检异常,必须处理)。
  • run()方法:仅线程执行体!直接调用等同于普通方法调用,不会创建新线程,所有逻辑在当前线程(比如主线程)同步执行,毫无并发效果。

🌰 实操案例:直观对比两者差异

结合集合遍历场景,看执行线程的区别:

import java.util.ArrayList;
import java.util.List;

class TaskRunnable implements Runnable {
    private List<String> dataList;

    public TaskRunnable(List<String> dataList) {
        this.dataList = dataList;
    }

    @Override
    public void run() {
        System.out.println("当前执行线程名称:" + Thread.currentThread().getName());
        for (String data : dataList) {
            System.out.println("处理数据:" + data);
        }
    }
}

public class StartVsRunDemo {
    public static void main(String[] args) {
        List<String> dataList = new ArrayList<>();
        dataList.add("Java多线程");
        dataList.add("IO流");
        dataList.add("集合框架");

        TaskRunnable task = new TaskRunnable(dataList);

        // 1. 直接调用run():无新线程,主线程执行
        System.out.println("===== 调用run() =====");
        task.run(); // 执行线程:main

        // 2. 调用start():创建新线程执行
        System.out.println("\n===== 调用start() =====");
        new Thread(task, "数据处理线程").start(); // 执行线程:数据处理线程

        // 以下代码会抛出异常(注释掉可测试)
        // Thread thread = new Thread(task);
        // thread.start();
        // thread.start(); // 重复调用start(),抛IllegalThreadStateException
    }
}

执行结果&结论

===== 调用run() =====
当前执行线程名称:main
处理数据:Java多线程
处理数据:IO流
处理数据:集合框架

===== 调用start() =====
当前执行线程名称:数据处理线程
处理数据:Java多线程
处理数据:IO流
处理数据:集合框架

👉 结论:只有start()能启动新线程实现并发,run()只是普通方法调用,面试遇到“如何启动线程”,直接答start(),并解释两者区别!

二、线程休眠:sleep()方法(控制执行节奏)

Thread.sleep(long millis) 让当前线程休眠指定毫秒数,休眠期间线程进入阻塞状态,释放CPU资源,但不会释放持有的锁。休眠结束后回到就绪状态,等待CPU重新调度。

核心注意点(避坑关键)

  1. 必须处理InterruptedException(受检异常),要么捕获要么声明抛出;
  2. 休眠时间是“最小等待时间”:休眠结束后需竞争CPU,实际执行时间可能略长于设定值;
  3. 不释放锁:如果线程持有synchronized锁,休眠期间其他线程无法获取该锁。

🌰 实操案例:IO分批读取文件

模拟“每读取3行文件休眠500ms”的业务场景,结合IO流实现:

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

class FileReadTask implements Runnable {
    private String filePath;

    public FileReadTask(String filePath) {
        this.filePath = filePath;
    }

    @Override
    public void run() {
        BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader(filePath));
            String line;
            int lineNum = 0;
            while ((line = br.readLine()) != null) {
                lineNum++;
                // 每3行休眠一次,模拟分批处理
                if (lineNum % 3 == 0) {
                    System.out.println(Thread.currentThread().getName() + " 分批处理中,休眠500ms");
                    Thread.sleep(500);
                }
                System.out.println(Thread.currentThread().getName() + " 读取内容:" + line);
            }
        } catch (IOException e) {
            System.out.println("文件读取异常:" + e.getMessage());
            e.printStackTrace();
        } catch (InterruptedException e) {
            System.out.println("线程休眠被中断:" + e.getMessage());
            e.printStackTrace();
        } finally {
            // 关闭流资源,避免内存泄漏
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

public class ThreadSleepDemo {
    public static void main(String[] args) {
        FileReadTask task = new FileReadTask("data.txt");
        new Thread(task, "文件读取线程1").start();
        new Thread(task, "文件读取线程2").start();
    }
}

三、线程插队:join()方法(保证执行顺序)

当需要让某个线程“优先执行完毕”,再继续执行当前线程时,用join()方法。比如主线程需要等待子线程统计完数据再汇总,避免“数据未统计完就计算”的错误。

核心作用

  • 调用thread.join():当前线程进入阻塞状态,直到目标线程thread执行完毕,当前线程才恢复执行;
  • 支持超时重载:join(long millis),超过指定时间后,当前线程自动恢复执行,无需等待目标线程结束。

🌰 实操案例:集合数据统计插队

主线程等待子线程统计完1-10的总和后,再进行汇总分析:

import java.util.ArrayList;
import java.util.List;

class DataCountTask implements Runnable {
    private List<Integer> numList;
    public int sum = 0; // 存储统计结果

    public DataCountTask(List<Integer> numList) {
        this.numList = numList;
    }

    @Override
    public void run() {
        for (Integer num : numList) {
            sum += num;
            try {
                Thread.sleep(100); // 模拟计算延迟
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + " 统计完成,总和:" + sum);
    }
}

public class ThreadJoinDemo {
    public static void main(String[] args) throws InterruptedException {
        // 初始化1-10的数字集合
        List<Integer> numList = new ArrayList<>();
        for (int i = 1; i <= 10; i++) {
            numList.add(i);
        }

        DataCountTask task = new DataCountTask(numList);
        Thread countThread = new Thread(task, "数据统计线程");
        countThread.start();

        System.out.println("主线程等待统计线程执行...");
        countThread.join(); // 主线程阻塞,等待统计线程完成
        System.out.println("主线程获取统计结果:" + task.sum);
        System.out.println("主线程开始汇总分析...");
    }
}

执行结果

主线程等待统计线程执行...
数据统计线程 统计完成,总和:55
主线程获取统计结果:55
主线程开始汇总分析...

👉 结论:join()是保证线程执行顺序的关键,面试常考“如何让线程A执行完再执行线程B”,答案就是B线程中调用A.join()!

四、线程让步:yield()方法(主动让出CPU)

Thread.yield() 让当前线程主动让出CPU资源,立即回到就绪状态,与其他就绪线程重新竞争CPU调度。

核心特点(与sleep()的区别)

方法核心特点锁资源异常处理
sleep()强制休眠指定时间,期间阻塞不释放需处理InterruptedException
yield()主动让出CPU,立即回到就绪状态不释放无需处理异常

注意点

  • 让步是“自愿行为”,但无法保证其他线程一定能获得CPU;
  • 让步后当前线程可能马上再次被CPU调度,比如只有一个就绪线程时;
  • 适用于“不需要优先执行”的线程,比如后台低优先级任务。

五、线程优先级:setPriority()(提示CPU调度)

Java线程优先级是“提示”CPU优先调度的机制,优先级高的线程获取CPU的概率更大,但不保证绝对先执行(CPU调度具有随机性)。

优先级范围(面试常考)

  • 最低优先级:Thread.MIN_PRIORITY(1);
  • 默认优先级:Thread.NORM_PRIORITY(5);
  • 最高优先级:Thread.MAX_PRIORITY(10)。

🌰 实操案例:设置线程优先级

class PriorityTask implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + 
                    " 优先级:" + Thread.currentThread().getPriority() + 
                    ",执行第" + (i+1) + "次");
        }
    }
}

public class ThreadPriorityDemo {
    public static void main(String[] args) {
        PriorityTask task = new PriorityTask();

        Thread lowPriorityThread = new Thread(task, "低优先级线程");
        Thread highPriorityThread = new Thread(task, "高优先级线程");

        // 设置优先级
        lowPriorityThread.setPriority(Thread.MIN_PRIORITY); // 1
        highPriorityThread.setPriority(Thread.MAX_PRIORITY); // 10

        lowPriorityThread.start();
        highPriorityThread.start();
    }
}

执行说明

多数情况下,高优先级线程会先执行,但偶尔也会出现低优先级线程先执行的情况——这是CPU调度随机性导致的,因此不能依赖优先级控制线程执行顺序,仅能作为辅助优化。

六、线程中断:interrupt()方法(优雅停止线程)

线程中断是Java中“优雅停止线程”的标准方式,它不会强制终止线程,而是给线程设置一个“中断标志”,线程可根据该标志自行决定是否停止。

核心逻辑

  1. 调用thread.interrupt():给线程设置中断标志为true;
  2. 线程中通过Thread.currentThread().isInterrupted()判断中断状态;
  3. 若线程处于sleep()/join()等阻塞状态,调用interrupt()会触发InterruptedException,同时清除中断标志。

🌰 实操案例:中断休眠中的线程

class InterruptTask implements Runnable {
    @Override
    public void run() {
        try {
            int i = 0;
            while (true) {
                System.out.println("线程执行中,次数:" + (++i));
                Thread.sleep(200); // 休眠状态可被中断

                // 检查中断标志,决定是否停止
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("检测到中断标志,准备停止线程");
                    break;
                }
            }
        } catch (InterruptedException e) {
            System.out.println("休眠被中断,线程准备停止");
            // 此处可处理中断后的资源释放逻辑
        }
        System.out.println("线程已停止执行");
    }
}

public class ThreadInterruptDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread taskThread = new Thread(new InterruptTask(), "测试中断线程");
        taskThread.start();

        // 主线程3秒后发起中断请求
        Thread.sleep(3000);
        System.out.println("主线程发起中断请求");
        taskThread.interrupt();
    }
}

执行结果

线程执行中,次数:1
线程执行中,次数:2
...(省略中间输出)
线程执行中,次数:15
主线程发起中断请求
休眠被中断,线程准备停止
线程已停止执行

👉 避坑:不要用stop()方法停止线程(已过时),会强制终止线程导致资源泄漏,interrupt()是优雅中断的首选!

七、今日作业+明日预告(进阶福利)

📝 课后挑战

join()sleep()结合,实现3个线程按“线程A→线程B→线程C”的顺序依次执行,要求每个线程执行完后,下一个线程才启动。

👉 GZH【咖啡 Java 研习班】,回复「学习资料」领取完整代码和多种实现方案。

📌 明日预告

当多个线程同时操作ArrayList、文件等共享资源时,会出现数据错乱、文件写入异常——这就是线程安全问题!明天我们拆解线程安全的本质,用synchronized解决共享资源竞争,结合集合、IO实战演练,还会揭秘面试常问的“ArrayList为什么线程不安全”!

最后说两句

线程控制是多线程开发的核心,也是面试高频考点,今天6大方法+实操案例覆盖了“启动、休眠、插队、让步、优先级、中断”的全场景。建议大家动手敲一遍代码,感受线程执行节奏的变化,尤其是start()和run()、sleep()和yield()的区别,记牢这些面试直接拿分!

如果觉得文章有用,欢迎点赞+收藏+转发,你的支持是我持续输出的动力~ 评论区留言你的作业完成情况,或者遇到的问题,一起交流进步!

GZH【咖啡 Java 研习班】,持续分享Java并发、JVM、SpringBoot等核心技术,助力你从初级工程师进阶到架构师~