缘博客
发布日期

Java多线程(二):线程调度、常用方法与生命周期

作者
  • 姓名
    社交账号

前言

会创建线程只是第一步。一个程序里出现多个线程以后,我们还会遇到一系列问题:怎样知道当前代码由哪个线程执行?能不能让线程暂停一会儿?如何等待另一个线程执行结束?线程优先级越高是否就一定先执行?

本篇围绕 Thread 的常用方法展开,并解释守护线程、礼让线程、插入线程和线程生命周期。理解这些内容后,我们才能正确分析一个线程此刻为什么没有继续运行。

一、线程名称与当前线程

有意义的线程名称非常重要。发生异常或分析日志时,Thread-0Thread-1 很难让人判断任务来源,而 order-save-1image-upload-2 一眼就能表达职责。

public class ThreadNameDemo {
    public static void main(String[] args) {
        Thread task = new Thread(() -> {
            Thread current = Thread.currentThread();
            System.out.println("当前线程:" + current.getName());
        });

        task.setName("文件上传线程");
        task.start();

        System.out.println("主线程:" + Thread.currentThread().getName());
    }
}

常用方法包括:

  • setName(String name):设置线程名称。
  • getName():获取线程名称。
  • Thread.currentThread():获取正在执行当前代码的线程对象。

如果没有主动命名,JVM 会为线程生成默认名称,但这只适合简单演示。实际开发中应尽量为线程或线程池设置可识别的名称。

二、sleep:让当前线程限时等待

Thread.sleep(long millis) 会让当前正在执行的线程休眠指定的毫秒数。

public class SleepDemo {
    public static void main(String[] args) {
        for (int i = 3; i >= 1; i--) {
            System.out.println(i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                System.out.println("倒计时被中断");
                return;
            }
        }
        System.out.println("开始!");
    }
}

使用 sleep() 时应理解四个细节:

  1. sleep() 是静态方法,影响的是当前线程,而不是“调用这个方法的对象”。推荐始终写成 Thread.sleep(...)
  2. 休眠时间到达只代表线程重新具备参与调度的资格,不代表它一定立即执行。
  3. sleep() 不会释放当前线程已经持有的锁。
  4. 其他线程可以通过 interrupt() 中断休眠,被中断的线程会收到 InterruptedException

捕获 InterruptedException 后重新调用 Thread.currentThread().interrupt(),可以恢复中断标记,让更上层代码知道线程曾被要求停止。学习示例里经常直接打印异常,但真实业务中不应该无条件吞掉中断信号。

三、线程优先级

Java 线程优先级范围是 1 到 10:

Thread.MIN_PRIORITY  // 1
Thread.NORM_PRIORITY // 5
Thread.MAX_PRIORITY  // 10
Thread t1 = new Thread(task, "低优先级线程");
Thread t2 = new Thread(task, "高优先级线程");

t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);

t1.start();
t2.start();

优先级高只表示它可能获得更多调度机会,绝不等于“高优先级线程一定先运行或先结束”。Java 线程最终要映射到操作系统的调度机制,不同平台的处理也可能不同。因此,不要把业务正确性建立在优先级上。凡是必须保证的执行顺序,应使用 join()、锁、队列等明确的协调手段。

四、守护线程

Java 线程可以分为用户线程和守护线程。主线程以及我们默认创建的线程通常属于用户线程;守护线程用于为其他线程提供后台服务。

public class DaemonDemo {
    public static void main(String[] args) {
        Thread backup = new Thread(() -> {
            int i = 1;
            while (true) {
                System.out.println("执行第 " + i++ + " 次后台备份");
            }
        }, "备份线程");

        backup.setDaemon(true);
        backup.start();

        for (int i = 1; i <= 5; i++) {
            System.out.println("用户线程执行:" + i);
        }
    }
}

当 JVM 中已经没有存活的用户线程时,JVM 可以结束运行,守护线程也会随之结束。它不会保证把剩余逻辑完整执行完,所以写文件收尾、保存订单、数据库提交等必须可靠完成的任务,不应该只依赖守护线程。

调用 setDaemon(true) 必须发生在 start() 之前,否则会抛出 IllegalThreadStateException

五、yield:礼让线程

Thread.yield() 表示当前线程愿意暂时让出 CPU 使用机会,让调度器重新选择可运行线程。

public class YieldDemo {
    public static void main(String[] args) {
        Runnable task = () -> {
            for (int i = 1; i <= 20; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
                Thread.yield();
            }
        };

        new Thread(task, "线程A").start();
        new Thread(task, "线程B").start();
    }
}

yield() 只是一种提示,调度器可以接受,也可以忽略。礼让之后,当前线程仍然可能再次获得执行权。因此它不提供确定的先后顺序,也不能用于实现可靠的线程通信。

六、join:等待指定线程结束

如果当前线程必须等另一个线程做完后才能继续,可以调用目标线程的 join()

public class JoinDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread download = new Thread(() -> {
            System.out.println("开始下载更新包");
            try {
                Thread.sleep(1500);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return;
            }
            System.out.println("更新包下载完成");
        }, "下载线程");

        download.start();
        download.join();
        System.out.println("开始安装更新包");
    }
}

主线程调用 download.join() 后,会等待 download 线程终止,再继续打印“开始安装”。课程里把它形象地称为“插入线程”,但从行为上看,更准确的理解是:当前线程等待目标线程结束。

join(long millis) 可以设置最长等待时间。如果在时间到达前目标线程仍未结束,当前线程也可以继续向下执行。

七、线程的生命周期

从业务视角看,线程大致会经历创建、就绪、运行、阻塞或等待、结束等阶段。Java API 中的 Thread.State 则定义了六种 JVM 线程状态。

1. NEW

线程对象已经创建,但还没有调用 start()

Thread thread = new Thread(task);
System.out.println(thread.getState()); // NEW

2. RUNNABLE

调用 start() 后,线程进入 RUNNABLE。Java 把操作系统层面的“就绪”和“正在运行”都归入这个状态,因此看到 RUNNABLE 并不能断定线程此刻正在占用 CPU。

3. BLOCKED

线程尝试进入 synchronized 保护的区域,却发现监视器锁正被其他线程占有,于是进入 BLOCKED。它等待的是“锁”。

4. WAITING

线程无限期等待另一个线程执行某个动作。例如调用无超时参数的 Object.wait()Thread.join(),都会进入 WAITING。它需要被唤醒或等待的目标线程结束,才可能继续运行。

5. TIMED_WAITING

线程在限定时间内等待,例如调用 Thread.sleep(1000)Object.wait(1000)Thread.join(1000)。时间到达后,它会重新进入可运行范围。

6. TERMINATED

run() 执行完毕,或者因为未处理异常而结束后,线程进入 TERMINATED。终止的线程不能再次调用 start()

八、BLOCKED、WAITING 与 TIMED_WAITING 的区别

这三个状态都表现为“线程暂时不继续执行”,但原因不同:

状态等待什么常见触发方式是否有超时
BLOCKED等待进入 synchronized 所需的监视器锁竞争内置锁失败没有主动超时参数
WAITING等待其他线程执行特定动作wait()join()无期限
TIMED_WAITING等待动作或时间到达sleep(n)wait(n)join(n)有期限

另一个关键区别是:sleep() 不释放已持有的锁,而 wait() 会释放调用对象的监视器锁。这个差异会在等待唤醒机制中继续展开。

九、观察线程状态

public class StateDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        System.out.println("创建后:" + worker.getState());
        worker.start();

        Thread.sleep(100);
        System.out.println("休眠时:" + worker.getState());

        worker.join();
        System.out.println("结束后:" + worker.getState());
    }
}

常见输出是 NEWTIMED_WAITINGTERMINATED。不过多线程调度具有不确定性,观察状态本身也存在时间差,诊断复杂问题时应结合线程转储和日志,而不是只读取一次 getState()

十、本篇小结

线程名称能够提高日志可读性;sleep() 让当前线程限时等待但不会释放锁;线程优先级和 yield() 都只影响调度倾向,不能保证业务顺序;守护线程适合后台辅助任务,不适合必须可靠收尾的工作;join() 则能明确表达“等待目标线程结束”。

Java 官方定义了 NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED 六种状态。下一篇将进入多线程最关键的部分:共享数据为什么会出错,以及如何使用同步代码块、同步方法和 Lock 保证线程安全。

参考资料