Java 线程从“萌新”到“躺平”:生命周期和调度机制真的这么玄乎?

举报
喵手 发表于 2025/12/08 20:21:26 2025/12/08
【摘要】 开篇语哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,...

开篇语

哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛

  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。

  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!

前言

说到多线程,大家大概率都经历过这么个阶段:

“线程?不就 new Thread().start() 嘛,有啥好说的?”

然后过一阵子,线上一波线程泄露CPU 飙满死锁奇怪卡顿,你开始怀疑人生:

“这些线程到底在干嘛?为什么有的在跑、有的在等、有的一直不结束?”

这时候,八成就是线程生命周期线程调度机制没搞明白。
今天我们就把这俩东西好好捋一遍:

  • 线程从出生到“退役”的完整生命周期到底长啥样?
  • Java 里的 NEW / RUNNABLE / BLOCKED / WAITING / TIMED_WAITING / TERMINATED 到底怎么来的?
  • 操作系统和 JVM 是怎么“排队安排线程上 CPU”的?
  • sleep()wait()join()yield() 都在调度里扮演什么角色?
  • 实战中有哪些坑,能提前避一避?

保证看完不会立刻变成“并发大佬”,但至少:

以后问你“线程有哪些状态、是怎么切换的”,你不会再只会说一句“就…Running 和 Dead 吧?”😅


一、线程是啥,先别急着上来就 start() 🚶‍♂️

简单粗暴地说:

  • 进程(Process):一个程序的“整体运行实例”,有自己独立的内存空间。
  • 线程(Thread):进程里的“执行单元”,共享进程的内存资源,可以同时干活。

对 Java 而言:

  • 一个 JVM 进程里可以有很多线程:GC 线程、应用线程、后台线程等等。
  • 每个 new Thread() 出来的家伙,底层都会映射成一个 操作系统级别的线程(HotSpot 经典实现)。
  • 线程调度由 操作系统 + JVM 一起配合 完成。

所以你可以把“线程生命周期”和“调度机制”理解为:
JVM 和 OS 一起养了一群线程,从出生、分配 CPU,到休眠、阻塞,最后收尸(回收)的一整套流程。


二、Java 线程生命周期:从出生到“终身退休”的那几步

Java 线程有一套比较“官方”的状态划分,Thread.State 枚举里写得很清楚:

public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

别看只有六个状态,组合起来能演四大文明史。我们一个一个拆开聊。

0. 先来一张“精神地图”🧠

用一张简化版“线程人生轨迹图”感受一下(ASCII 版凑合看下):

          NEW
           |
           v
       start()
           |
           v
       RUNNABLE <-------------------------------+
        /   |   \                               |
       /    |    \                              |
      v     v     v                             |
 BLOCKED  WAITING  TIMED_WAITING                |
   ^        ^           ^                       |
   |        |           |                       |
   |        |           +-- sleep()/wait(timeout)/join(timeout)
   |        +-- wait()/join()/park()           |
   +-- 获得锁                                  |
                                               |
                    run() 方法执行完 / 抛异常  |
                               v               |
                           TERMINATED ---------+

下面我们用“故事模式”来解释每个状态。


1. NEW:刚出生但还没开始干活 👶

Thread t = new Thread(() -> System.out.println("hello"));
System.out.println(t.getState()); // NEW

特点:

  • 线程对象已经创建,但是还没调用 start()
  • 这时候线程还没被操作系统“登记”,更别提调度上 CPU 了。

注意:调用 run() 不会改变状态为 RUNNABLE,这只是一个普通方法调用:

Thread t = new Thread(() -> System.out.println(Thread.currentThread().getName()));
t.run(); // 这行只是当前线程(比如 main)执行 run 方法

只有调用 start(),才会:

  • 请求 JVM 建立一个新的操作系统线程
  • 让它进入调度队列

2. RUNNABLE:在跑,或者等着跑 🏃‍♂️

在 Java 里,RUNNABLE 状态有点“综合症”性质:

表示线程要么正在运行,要么正在就绪队列里排队,等待被操作系统调度执行。

Thread t = new Thread(() -> {
    while (true) {
        // do sth
    }
});
t.start();
System.out.println(t.getState()); // 很可能是 RUNNABLE

特点:

  • RUNNABLE ≈ OS 级别的 Ready + Running 状态的统称。
  • 线程可能真的在 CPU 上跑,也可能刚被抢占、等下一个时间片。

线程什么时候会变成 RUNNABLE?

  • NEW 调用 start() 之后
  • BLOCKED 拿到锁之后
  • WAITING 被唤醒后
  • TIMED_WAITING 超时 / 被唤醒后

3. BLOCKED:等锁等到怀疑人生 🔒

当线程想进入某个 synchronized 受保护的代码块 / 方法,但锁被别人占着,就会变成 BLOCKED

public class BlockedDemo {
    private static final Object LOCK = new Object();

    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(() -> {
            synchronized (LOCK) {
                try {
                    Thread.sleep(5000); // 占用锁 5 秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (LOCK) {
                System.out.println("t2 got lock");
            }
        });

        t1.start();
        Thread.sleep(100); // 确保 t1 先拿到锁
        t2.start();

        Thread.sleep(1000);
        System.out.println("t2 state = " + t2.getState()); // 大概率是 BLOCKED
    }
}

特点:

  • BLOCKED 只针对 synchronized 的 monitor 锁
  • 是在等“锁的进入许可”,而不是因为 wait()sleep() 之类。
  • 一旦拿到锁,就会回到 RUNNABLE 状态。

简单说:

  • 想进 synchronized,结果有人在里面蹲着不出来 → 你就 BLOCKED。

4. WAITING:没人叫你,你就一直等着 🧘‍♂️

WAITING 是“无限期等待”,常见原因有:

  • Object.wait() (不带超时)
  • Thread.join() (不带超时)
  • LockSupport.park()

来看个 wait/notify 的例子:

public class WaitingDemo {
    private static final Object LOCK = new Object();

    public static void main(String[] args) throws Exception {
        Thread waiter = new Thread(() -> {
            synchronized (LOCK) {
                try {
                    System.out.println("waiter: going to wait...");
                    LOCK.wait(); // 进入 WAITING
                    System.out.println("waiter: awakened");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        waiter.start();
        Thread.sleep(500); // 确保 waiter 先 wait
        System.out.println("waiter state = " + waiter.getState()); // WAITING

        Thread notifier = new Thread(() -> {
            synchronized (LOCK) {
                System.out.println("notifier: notify one");
                LOCK.notify();
            }
        });
        notifier.start();
    }
}

特点:

  • 线程主动说:“我等着,什么时候有人叫我(notify/join 完成/unpark),我再继续。”
  • 没超时时间,理论上你不叫,它就一直等,直到线程被中断或者 notify。

5. TIMED_WAITING:我等你一会,但别超过时间 ⏳

TIMED_WAITING 是“带时间限制的等待”,常见来源:

  • Thread.sleep(millis)
  • Object.wait(timeout)
  • Thread.join(timeout)
  • LockSupport.parkNanos() / parkUntil()

例子:

public class TimedWaitingDemo {
    public static void main(String[] args) throws Exception {
        Thread sleeper = new Thread(() -> {
            try {
                Thread.sleep(3000); // TIMED_WAITING
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        sleeper.start();
        Thread.sleep(500);
        System.out.println("sleeper state = " + sleeper.getState()); // TIMED_WAITING
    }
}

特点:

  • 线程在“睡觉”或者“限时等某件事发生”,超时后自动回到 RUNNABLE 队列。
  • 多用于:超时等待锁、超时等待 IO、定时任务等场景。

6. TERMINATED:线程人生走完了 💀

当线程的 run() 方法执行结束,或者抛出未捕获异常,线程就进入 TERMINATED 状态。

public class TerminatedDemo {
    public static void main(String[] args) throws Exception {
        Thread t = new Thread(() -> System.out.println("do something"));
        t.start();
        Thread.sleep(100);
        System.out.println(t.getState()); // 基本就是 TERMINATED 了
    }
}

特点:

  • 线程结束后,就不能再启动了,调用 start() 会直接抛异常:
t.start();
t.start(); // 会抛 IllegalThreadStateException

一个线程对象的生命周期,只能经历一次:
NEW → … → TERMINATED,不能重来。


三、来一段完整 demo,看状态如何真实变换 🎬

我们写一段小代码,一次性把几个状态串起来看:

public class ThreadLifeCycleDemo {

    private static final Object LOCK = new Object();

    public static void main(String[] args) throws Exception {
        Thread t = new Thread(() -> {
            System.out.println("1. 子线程开始,当前状态:" + Thread.currentThread().getState());

            // sleep -> TIMED_WAITING
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            synchronized (LOCK) {
                try {
                    // wait -> WAITING
                    LOCK.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            System.out.println("2. 子线程被唤醒,准备结束");
        }, "demo-thread");

        System.out.println("main: t state = " + t.getState()); // NEW

        t.start();
        Thread.sleep(100); // 确保子线程进入 sleep
        System.out.println("main: t state after start = " + t.getState()); // RUNNABLE or TIMED_WAITING

        Thread.sleep(600); // 此时 sleep 已结束,进入 wait
        System.out.println("main: t state after wait = " + t.getState()); // WAITING

        synchronized (LOCK) {
            LOCK.notify(); // 唤醒子线程
        }

        Thread.sleep(100);
        System.out.println("main: t final state = " + t.getState()); // TERMINATED
    }
}

当然,每次输出的状态可能会略有差异(调度是动态的),
但大体能看到一条“从 NEW 出生,一路折腾到 TERMINATED”的主线。


四、线程调度机制:谁能上 CPU,不是你说了算 🧮

线程状态搞清楚之后,另一个问题自然来了:

“这么多线程,CPU 就几个核,谁先跑?跑多久?怎么换?”

这就涉及线程调度(Thread Scheduling)

1. 谁在做调度?OS + JVM 双人舞

现实情况是这样的:

  • 操作系统 负责真正的 CPU 调度:给哪个线程时间片、什么时候切线程。

  • JVM 只是:

    • 把 Java 线程映射到 OS 线程
    • 给 OS 一些“软建议”(比如线程优先级)
    • 管理自己的线程对象、状态

你可以粗略理解为:

Java 把一堆线程交给系统说:
“哥们,这几个是我的,你按规则帮我轮着跑哈,我在旁边看状态就行。”


2. 调度策略:抢占式 + 时间片轮转

主流桌面/服务器操作系统(Windows、Linux 等)都是抢占式调度

  • 每个线程分配一个时间片(几十毫秒级别)。
  • 用完或者被更高优先级线程抢占,就切出。
  • 如果一个线程自己调用 sleep() / wait() / park(),会主动放弃 CPU。

Java 层面的 RUNNABLE 状态就是在 OS 调度层面:

  • 要么是 Ready(等着)
  • 要么是 Running(跑着)

3. 线程优先级:说是“优先”,其实只是“建议”📢

Java 提供了 10 个优先级:

public static final int MIN_PRIORITY = 1;
public static final int NORM_PRIORITY = 5;
public static final int MAX_PRIORITY = 10;

你可以这么设置优先级:

Thread high = new Thread(task, "high");
Thread low = new Thread(task, "low");

high.setPriority(Thread.MAX_PRIORITY);
low.setPriority(Thread.MIN_PRIORITY);

坑点在于:

  • 优先级是对操作系统的“建议”,不是硬性规定。
  • 不同 OS、不同 JVM 实现下表现差异很大。
  • 过度依赖优先级实现“某个线程一定比另一个先执行”,基本就是在和运气谈恋爱。

实战建议:
除非特别明确的场景(比如某些后台低优先级线程),一般乖乖用默认优先级就行。


4. 哪些方法会影响调度?

几个常见的 Java API,本质就是在给调度器发信号:

(1)Thread.sleep(ms):我暂时不干活,让别人跑一会

  • 当前线程进入 TIMED_WAITING,在这段时间里不会占用 CPU。
  • 时间到了之后,回到 RUNNABLE 队列,等调度。

常用于:

  • 定时轮询
  • 降低 CPU 空转
  • Demo 人为“放慢节奏”

(2)Thread.yield():我手头活不急,让同优先级的哥们先上

  • 向调度器暗示:当前线程愿意让出 CPU。

  • 实际上:

    • 可能会让别的同优先级线程运行
    • 也可能啥也不发生(调度器不理你)

简单说:yield() 是“请求让步”,不保证成功。

(3)join():我等你干完活再继续

Thread worker = new Thread(() -> doWork());
worker.start();
worker.join(); // 当前线程进入 WAITING / TIMED_WAITING
  • 当前线程挂起,直到目标线程执行完毕(TERMINATED)或超时。

(4)wait()/notify():基于对象监视器的协作

  • wait():释放对象锁,进入 WAITING/TIMED_WAITING
  • notify() / notifyAll():唤醒在该对象监视器上等待的线程

配合 synchronized 使用,是经典的生产者-消费者写法原始工具。

(5)LockSupport.park()/unpark():更底层的“停车/放行”

很多并发工具类(java.util.concurrent 包里的)都是用它构建的,例如:

  • AbstractQueuedSynchronizer(AQS)
  • ReentrantLock
  • CountDownLatch

五、调度相关常见坑:CPU 炸、线程挂、活儿没人干 😵

1. 忙等(busy-wait):看起来很努力,实际上巨浪费

典型反例:

while (!condition) {
    // 啥也不干,就一直转
}
  • 这种写法会一直占着 CPU 时间片,拼命轮询。
  • 在多核机器上,很容易把某个核打满。

更合理的写法应该是:

  • 要么用 wait/notify
  • 要么用 LockSupport.park/unpark
  • 要么用 Condition.await/signal

例如:

synchronized (LOCK) {
    while (!condition) {
        LOCK.wait(); // 进入 WAITING,释放锁
    }
}

2. 锁竞争严重导致线程大量 BLOCKED

如果你看到线程 dump 里一堆:

"worker-1" BLOCKED on ...
"worker-2" BLOCKED on ...
"worker-3" BLOCKED on ...

多半是某个 synchronized 块太粗:

public synchronized void handle() {
    // 里面干活时间巨长
}

或者锁粒度过大,把不相关的操作都绑死在一个锁上。

解决思路:

  • 缩小锁的粒度
  • 用更细粒度的锁(分段锁)
  • 使用并发容器 / CAS 等减少锁竞争

3. 线程饥饿(starvation):你永远轮不到

当某些线程一直拿不到锁 / 一直被高优先级线程“压着”,可能出现饥饿:

  • 同一个锁总是被少数线程抢走
  • 低优先级线程迟迟无法获得 CPU

避免饥饿的一些做法:

  • 锁设计时尽量避免偏向某个固定线程
  • 使用公平锁(如 new ReentrantLock(true)),但要注意性能代价
  • 不要滥用线程优先级

4. 死锁(deadlock):相互等锁,大家一起躺平

虽然死锁不完全是“调度问题”,但最后表现就是:

  • 多个线程一直 BLOCKED,互相拿着对方要的锁
  • 整个系统部分逻辑完全停摆

经典例子:

public class DeadLockDemo {

    private static final Object A = new Object();
    private static final Object B = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (A) {
                sleep(100);
                synchronized (B) {
                    System.out.println("t1 got A and B");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (B) {
                sleep(100);
                synchronized (A) {
                    System.out.println("t2 got B and A");
                }
            }
        });

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

    private static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
    }
}

产生死锁后:

  • t1 拿着 A 等 B
  • t2 拿着 B 等 A
  • 谁也等不到,调度器也救不了你。

避免死锁的实战策略:约定统一的加锁顺序、减少嵌套锁、使用显式锁 + tryLock 等等。


六、实战推荐:怎么配合线程生命周期 & 调度写出不太作死的代码?🛠

1. 多数情况下,用线程池比自己管线程靠谱

ExecutorService pool = Executors.newFixedThreadPool(4);

pool.submit(() -> {
    // 你的任务逻辑
});

线程池的好处:

  • 统一管理线程生命周期(创建、复用、销毁)
  • 避免频繁 new Thread 带来的系统资源消耗
  • 提供任务队列、拒绝策略等机制

你只管任务,线程的调度细节交给 JVM + OS + 线程池。


2. 不要用线程优先级实现“业务逻辑”

例如:

highPriorityThread.setPriority(Thread.MAX_PRIORITY);
lowPriorityThread.setPriority(Thread.MIN_PRIORITY);

然后指望:

  • “高优先级的线程一定先执行完”
  • “低优先级一定最后跑”

大概率会翻车。

正确姿势

  • 用显式的任务队列、排队规则、锁、信号量等机制控制先后顺序。
  • 把优先级当作系统调优的一个小参数,而非业务语义的一部分。

3. 正确处理中断:配合调度机制优雅停止线程

线程可能被别的线程调用 interrupt(),表示“建议你停一停”:

public class InterruptDemo {
    public static void main(String[] args) throws Exception {
        Thread worker = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    // 模拟工作
                    Thread.sleep(1000);
                    System.out.println("working...");
                } catch (InterruptedException e) {
                    // sleep 被中断会清除中断标志,这里要重新设置
                    Thread.currentThread().interrupt();
                }
            }
            System.out.println("worker stopped");
        });

        worker.start();
        Thread.sleep(2500);
        worker.interrupt(); // 请求停止
    }
}
  • 中断配合 sleep / wait 等阻塞操作,本质是在“调度层面优雅地停掉线程”。
  • 合理处理中断,是写出“可控生命周期线程”的关键一步。

七、收个尾:线程是“活人”,不是“黑盒” 🧩

我们从头到尾,大致走了一遍:

  1. 线程生命周期

    • NEW 出生,到 RUNNABLE 干活,
    • 再到 BLOCKED/WAITING/TIMED_WAITING 各种“排队排坑”,
    • 最后 TERMINATED 收尾。
  2. 线程调度机制

    • OS + JVM 合作,抢占式 + 时间片轮转,
    • 优先级只是“建议”,别迷信。
  3. 调度相关 API

    • sleepyieldjoinwait/notifypark/unpark
    • 背后都是在影响线程在不同状态之间切换。
  4. 常见坑

    • 忙等把 CPU 炸穿
    • 锁竞争 & 饥饿
    • 死锁导致一群线程一起躺平
  5. 实战建议

    • 多用线程池,少自己管线程;
    • 不依赖优先级实现业务顺序;
    • 正确处理中断;
    • 把反复出问题的地方丢给并发工具类(java.util.concurrent

… …

文末

好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。

… …

学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!

wished for you successed !!!


⭐️若喜欢我,就请关注我叭。

⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。


版权声明:本文由作者原创,转载请注明出处,谢谢支持!

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。