JUC并发编程03 - 线程方法(02)线程原理/线程状态/查看线程

Java线程原理与状态解析

Thread 类 API

终止模式

想象你正在做一个家务活,比如洗碗。突然妈妈叫你去吃饭,这时候你会怎么做?直接放下碗就走吗?当然不会,你会先把碗里的水倒掉,把碗放回原位,然后再去吃饭。这就是一种“优雅”的终止方式——先处理好当前的事情,再结束任务。

在编程中,线程就像你在做家务,有时候我们需要让线程停下来去做别的事情(比如吃饭)。如果我们直接强制停止线程(相当于直接扔下碗就走),可能会导致一些问题,比如资源没有释放干净,其他线程无法继续使用这些资源。

两阶段终止模式就是一种优雅地让线程停止的方法,它分为两个步骤:

  1. 通知线程该停止了:就像妈妈叫你去吃饭一样,告诉线程“你可以准备结束了”。
  2. 线程自己处理后事:线程收到通知后,会先完成手头的工作,释放资源,然后再真正停止。

错误的做法

  • 使用 stop() 方法:这就像直接把你从厨房里拽出来,不管碗还在不在水槽里。这样会导致资源泄露,其他线程可能永远无法使用这些资源。
  • 使用 System.exit(int) 方法:这就像直接关掉整个房子的电,不仅你不能继续洗碗,连其他人都不能做任何事情了。

终止模式之两阶段终止模式:Two Phase Termination

目标:在一个线程 T1 中如何优雅终止线程 T2?优雅指的是给 T2 一个后置处理器

错误思想:

  • 使用线程对象的 stop() 方法停止线程:stop 方法会真正杀死线程,如果这时线程锁住了共享资源,当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁
  • 使用 System.exit(int) 方法停止线程:目的仅是停止一个线程,但这种做法会让整个程序都停止

正确的做法:两阶段终止模式

我们来看一个具体的例子,假设有一个监控线程,它的任务是每隔一段时间记录一些数据。现在我们需要让它停止工作。

package com.cg.jucproject.demo;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination tpt = new TwoPhaseTermination();
        tpt.start(); // 启动监控线程
        Thread.sleep(3500); // 等待一段时间
        tpt.stop(); // 停止监控线程
    }
}

class TwoPhaseTermination {
    private Thread monitor;

    // 启动监控线程
    public void start() {
        monitor = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    Thread thread = Thread.currentThread();
                    if (thread.isInterrupted()) { // 检查是否被打断
                        System.out.println("后置处理");
                        break; // 结束循环
                    }
                    try {
                        Thread.sleep(1000); // 睡眠1秒
                        System.out.println("执行监控记录"); // 执行监控任务
                    } catch (InterruptedException e) { // 在睡眠期间被打断
                        e.printStackTrace();
                        // 重新设置打断标记,因为打断 sleep 会清除打断状态
                        thread.interrupt();
                    }
                }
            }
        });
        monitor.start();
    }

    // 停止监控线程
    public void stop() {
        monitor.interrupt(); // 通知线程该停止了
    }
}

代码输出如下:

  1. 执行监控记录(出现3次)

    • 这是线程在正常工作。main 方法启动监控线程 (tpt.start()) 后,让它运行了 3500 毫秒(3.5秒)。
    • 监控线程的循环里,每次 sleep(1000) 睡1秒,醒来后打印 执行监控记录
    • 3.5秒内,它完整地执行了3个周期(睡1秒 -> 打印 -> 睡1秒 -> 打印 -> 睡1秒 -> 打印),所以打印了3次。
  2. java.lang.InterruptedException: sleep interrupted ...

    • 当 main 线程执行到 tpt.stop() 时,它调用了 monitor.interrupt()
    • 此时,监控线程正在执行第4次的 Thread.sleep(1000),也就是它正处于“睡眠”状态。
    • 关键点:对一个正在 sleep 的线程调用 interrupt(),会立即唤醒它,并抛出 InterruptedException
    • 这个异常就是在 catch (InterruptedException e) 块中被捕获并打印出来的。这不是错误,而是预期的行为,表明“中断信号”在 sleep 期间被接收到了。
  3. 后置处理

    • 在 catch 块中,代码执行了 thread.interrupt();。这一步至关重要
      • 为什么需要这一步?:当 sleep() 因为被中断而抛出 InterruptedException 时,JVM 会自动将该线程的“中断状态”清除(即 isInterrupted() 会重新变成 false)。如果不手动重新设置,下一次循环时 if (thread.isInterrupted()) 就会是 false,线程会继续循环下去,无法真正停止!
    • thread.interrupt(); 重新设置了中断状态。
    • catch 块执行完毕后,循环继续。进入下一次 while(true) 循环的开头,if (thread.isInterrupted()) 此时为 true(因为刚被重新设置),于是执行 System.out.println("后置处理"); 并 break,线程优雅退出。

图示如下:

daemon(守护线程)

守护线程就像“服务员”,用户线程是“客人”。只要最后一个客人走了,不管服务员手头活干完没,都得关门下班!

什么是用户线程?什么是守护线程?

类型英文名大白话解释
用户线程User Thread你写的普通线程,比如 new Thread(() -> {...}),它是“主角”,JVM 会等它执行完才退出。
守护线程Daemon Thread它是“配角”、“后勤人员”,默默服务别人。一旦所有“主角”都走了,它就算没干完活,也得立刻停止,JVM 直接关门!

关键规则(记住这句):

JVM 只有在还有用户线程运行时才会继续运行。一旦所有用户线程结束,不管守护线程有没有执行完,JVM 都会退出。

怎么设置守护线程?

setDaemon(true)必须在线程启动前设置!

Thread t = new Thread(() -> {
    while (true) {
        System.out.println("我是守护线程,我在默默工作...");
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            break;
        }
    }
});

// 正确:启动前设置
t.setDaemon(true); // 标记为守护线程
t.start();         // 启动

错误写法:

t.start();
t.setDaemon(true); // 报错!线程已启动,不能再设为守护线程!

接下来写一个只有用户线程的demo(JVM 不会退出)

public class UserThreadDemo {
    public static void main(String[] args) {
        Thread userThread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("用户线程:第 " + i + " 次打印");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 不设置 daemon,默认就是用户线程
        userThread.start();

        System.out.println("main 线程结束了");
    }
}

代码输出如下:

JVM 会等 userThread 打印完 5 次才退出。

再来写一个守护线程 + 用户线程的demo(守护线程会被强制结束):

public class DaemonThreadDemo {
    public static void main(String[] args) {
        Thread daemonThread = new Thread(() -> {
            while (true) { // 无限循环,想一直打印
                System.out.println("守护线程:我还活着...");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    break;
                }
            }
        });

        // 设置为守护线程
        daemonThread.setDaemon(true);
        daemonThread.start();

        // 模拟用户线程工作几秒就结束
        Thread userThread = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("我是用户线程,我还在工作...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("用户线程:我干完活了,拜拜!");
        });

        userThread.start();
    }
}

代码输出如下:

虽然 daemonThread 想一直打印,但 userThread 结束后,JVM 发现没有用户线程了,立刻终止守护线程,程序结束。所以你不会看到“守护线程:我还活着...”一直打印下去。

总结一下:

public final void setDaemon(boolean on):如果是 true ,将此线程标记为守护线程,线程启动前调用此方法。

用户线程:平常创建的普通线程

守护线程:服务于用户线程,只要其它非守护线程运行结束了,即使守护线程代码没有执行完,也会强制结束。守护进程是脱离于终端并且在后台运行的进程,脱离终端是为了避免在执行的过程中的信息在终端上显示

说明:当运行的线程都是守护线程,Java 虚拟机将退出,因为普通线程执行完后,JVM 是守护线程,不会继续运行下去

常见的守护线程:

  • 垃圾回收器线程就是一种守护线程
  • Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求

不推荐

不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁:

  • public final void stop():停止线程运行

    废弃原因:方法粗暴,除非可能执行 finally 代码块以及释放 synchronized 外,线程将直接被终止,如果线程持有 JUC 的互斥锁可能导致锁来不及释放,造成其他线程永远等待的局面

  • public final void suspend()挂起(暂停)线程运行

    废弃原因:如果目标线程在暂停时对系统资源持有锁,则在目标线程恢复之前没有线程可以访问该资源,如果恢复目标线程的线程在调用 resume 之前会尝试访问此共享资源,则会导致死锁

  • public final void resume():恢复线程运行

线程原理

运行原理

Java Virtual Machine Stacks(Java 虚拟机栈):每个线程启动后,虚拟机就会为其分配一块栈内存

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

想象你是一个程序员,你每做一件事(比如写代码、开会、吃饭),都会在自己的笔记本上记下当前在做什么、变量是多少、下一步去哪

在 Java 中:

每个线程就像一个独立的程序员,JVM 会给它发一个“私人笔记本” — 这就是“虚拟机栈”。

栈是怎么工作的?— “叠便签纸”

这个笔记本有个特点:你只能从最上面写和擦(后进先出,LIFO)。

  • 每当你调用一个方法,比如 main() 调用 a()a() 调用 b()
    • JVM 就给你这个线程的栈压入一张新的“便签纸”,这张纸就叫 栈帧(Stack Frame)
  • 每张便签纸上写着:
    • 这个方法的局部变量(比如 int n = 10;
    • 方法执行到哪一行了(返回地址
    • 中间计算用的临时数据(操作数栈
    public static void main(String[] args) {
        a();
    }
    
    static void a() {
        int x = 10;
        b();
    }
    
    static void b() {
        int y = 20;
        System.out.println(y);
    }

    线程执行过程:

    栈顶(当前正在执行)栈内容(从上到下)
    b() 的栈帧(y=20)← 活动栈帧(唯一一个正在执行的)
    a() 的栈帧(x=10)
    main() 的栈帧
    ...

    每个线程只能有一个“活动栈帧”:就是当前正在执行的方法(这里是 b())。

    其他方法都“暂停”在下面,等上面的执行完再继续。

    线程上下文切换(Thread Context Switch):一些原因导致 CPU 不再执行当前线程,转而执行另一个线程

    • 线程的 CPU 时间片用完
    • 垃圾回收
    • 有更高优先级的线程需要运行
    • 线程自己调用了 sleep、yield、wait、join、park 等方法

    线程上下文切换 — CPU 的“换人上岗”

    你是一个 CPU,同一时间只能干一件事。

    现在有多个线程(多个任务)排队等你处理。你不能一直干一个活,得轮流来

    上下文切换 = 你从干 A 的活,换成干 B 的活。

    但换人之前,你得:

    1. 把 A 的工作进度、笔记、状态都收好打包(保存)
    2. 把 B 的包拿出来,恢复他的笔记和进度
    3. 开始干 B 的活

    这个“收包 + 换包”的过程,就是 上下文切换

    什么时候会发生上下文切换?

    原因解释
    CPU 时间片用完A 干了 10ms,系统说:“你歇会儿,让别人干干。”
    垃圾回收(GC)JVM 突然要打扫内存,所有线程暂停,GC 线程上场。
    高优先级线程来了领导来了,普通员工先停下,让领导先办。
    线程自己睡了A 自己调了 sleep() 或 wait(),说:“我困了,先歇会儿。”

    切换时要保存啥?

    操作系统会把当前线程的“全部家当”存到 PCB(进程控制块) 里,包括:

    • 程序计数器(PC Register):记住下一条要执行的指令地址(就像书签)
    • 虚拟机栈的所有栈帧:每个方法的局部变量、返回地址等
    • 寄存器状态、堆栈指针等

    换回来时,再把这些“家当”还给它,接着干。

    程序计数器(Program Counter Register):记住下一条 JVM 指令的执行地址,是线程私有的

    当 Context Switch 发生时,需要由操作系统保存当前线程的状态(PCB 中),并恢复另一个线程的状态,包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等

    程序计数器 — 线程的“书签”

    你正在看书(执行代码),看到第 38 页第 5 行。

    突然你妈叫你吃饭,你合上书,放个书签在第 38 页。

    吃完饭回来,你一看书签,就知道从哪继续看。

    程序计数器就是线程的“书签”!

    • 它记录:下一条要执行的 JVM 字节码指令的地址
    • 每个线程都有自己的程序计数器(线程私有)。
    • 只有执行 Java 方法时,它才记录地址;执行 native 方法时,它的值是 undefined。

    JVM 规范并没有限定线程模型,以 HotSopot 为例:

    • Java 的线程是内核级线程(1:1 线程模型),每个 Java 线程都映射到一个操作系统原生线程,需要消耗一定的内核资源(堆栈)
    • 线程的调度是在内核态运行的,而线程中的代码是在用户态运行,所以线程切换(状态改变)会导致用户与内核态转换进行系统调用,这是非常消耗性能

    HotSpot 的线程模型(1:1 模型)— Java 线程与操作系统线程

    Java 的线程不是“虚拟”的,它是真实映射到操作系统线程的。

    一个 Java 线程 = 一个操作系统原生线程(1:1 模型)

    这就像是:

    • 你公司招一个程序员(Java 线程)
    • 操作系统就真给这个人发工牌、安排工位(内核线程)

    缺点:切换太贵!

    因为 Java 线程直接对应操作系统线程,所以:

    • 线程调度由操作系统内核负责(在“内核态”运行)
    • 但你的代码是在“用户态”运行
    • 每次切换线程,就要从“用户态”进入“内核态”,做系统调用

    这个过程非常耗性能!就像:

    你想让两个员工换班,得先叫人事经理(内核) 来登记、交接、签字,非常麻烦。

    所以:

    • 线程不能创建太多(否则系统忙于切换,真正干活的时间少了)
    • 这也是为什么后来有了 协程(如 Kotlin 协程、Quasar),它们是“用户态线程”,切换更快。

    Java 中 main 方法启动的是一个进程也是一个主线程,main 方法里面的其他线程均为子线程,main 线程是这些线程的父线程

    public class Main {
        public static void main(String[] args) {
            Thread t = new Thread(() -> {
                System.out.println("我是子线程");
            });
            t.start(); // 启动子线程
            System.out.println("main线程结束");
        }
    }
    • main() 方法启动的是一个 进程,同时也是这个进程里的 主线程
    • 在 main 中创建的线程(如 t),叫 子线程
    • main 线程是这些子线程的 父线程

    注意:main 线程结束,程序不一定结束!

    只要还有用户线程在运行(比如 t 是普通线程),JVM 就不会退出。

    但如果 t守护线程,main 结束后,JVM 会立刻退出,t 也会被终止。

    线程调度

    线程调度指系统为线程分配处理器使用权的过程,方式有两种:协同式线程调度、抢占式线程调度(Java 选择)

    想象你是一个 CPU,特别能干,但同一时间只能做一件事

    现在有好几个线程(任务)排队等你处理,比如:

    • 线程A:下载电影
    • 线程B:播放音乐
    • 线程C:打游戏

    你不能同时干三件事,那怎么办?系统(操作系统)要决定:先让谁上,干多久,什么时候换人。这个“安排谁上场干活”的过程,就叫 线程调度

    协同式线程调度:线程的执行时间由线程本身控制

    • 优点:线程做完任务才通知系统切换到其他线程,相当于所有线程串行执行,不会出现线程同步问题
    • 缺点:线程执行时间不可控,如果代码编写出现问题,可能导致程序一直阻塞,引起系统的奔溃

    协同式调度  — “自觉交接班制”

    公司规定:谁干活,自己说了算。
    你干完了,或者你觉得该歇了,就主动喊一声:“我干完了,下一个上!”

    这就是 协同式调度:线程自己控制执行时间,自己决定啥时候让出 CPU

    优点:

    • 不用管理员(系统)操心,大家自己协调。
    • 没有“抢活干”的冲突,不会出现同步问题(比如两个人同时改同一个文件)。

    缺点:

    • 万一有人不自觉呢?
      • 比如你打游戏上头了,说:“再玩一局,就一局!” 结果玩了10局……
      • 那你弟就一直等着,音乐也停了,下载也卡了。
      • 整个系统就卡死了!

    所以这种调度方式风险高,Java 没采用它

    抢占式线程调度:线程的执行时间由系统分配

    • 优点:线程执行时间可控,不会因为一个线程的问题而导致整体系统不可用
    • 缺点:无法主动为某个线程多分配时间

    抢占式调度 — “定时打卡换班制”

    公司现在改制度了:每人最多干10分钟,时间一到,不管干没干完,都必须下台!

    谁说了算?管理员(操作系统)说了算!

    这就是 抢占式调度:执行时间由系统分配,时间一到,强行换人

    优点:

    • 公平!可控!
    • 即使你写了个死循环(while(true)),系统也会强行把你“踢下去”,不会让整个程序卡死。
    • 系统稳定,不会因为一个线程出问题就崩了。

    缺点:

    • 你不能说:“这任务特别重要,让我多干会儿。”
    • 系统只认时间片,不认“感情”,无法主动给某个线程更多时间

    Java 用的就是这种调度方式!

    Java 提供了线程优先级的机制,优先级会提示(hint)调度器优先调度该线程,但这仅仅是一个提示,调度器可以忽略它。在线程的就绪状态时,如果 CPU 比较忙,那么优先级高的线程会获得更多的时间片,但 CPU 闲时,优先级几乎没作用

    说明:并不能通过优先级来判断线程执行的先后顺序

    虽然不能“加时赛”,但 Java 提供了一个“小建议”机制:线程优先级

    你有两个任务:

    • 任务A:紧急 bug 修复(优先级高)
    • 任务B:日常打扫(优先级低)

    你跟管理员说:“能不能多照顾一下 A?”

    管理员说:“行,我尽量。”

    但注意:这只是个‘建议’,不是命令!
    管理员(操作系统)可以听,也可以不听

    • 当 CPU 很忙时(任务多),优先级高的线程会获得更多时间片,大概率先执行。
    • 当 CPU 很闲时(就两个线程),优先级几乎没区别,反正都有时间干。
    • 不能靠优先级判断谁先执行! 因为最终决定权在操作系统。
    Thread t1 = new Thread(() -> System.out.println("高优先级"));
    Thread t2 = new Thread(() -> System.out.println("低优先级"));
    
    t1.setPriority(Thread.MAX_PRIORITY); // 10
    t2.setPriority(Thread.MIN_PRIORITY); // 1
    
    t1.start();
    t2.start();

    输出顺序不确定!可能是 t1 先,也可能是 t2 先。优先级只是“提示”。

    未来优化

    内核级线程调度的成本较大,所以引入了更轻量级的协程。用户线程的调度由用户自己实现(多对一的线程模型,多个用户线程映射到一个内核级线程),被设计为协同式调度,所以叫协程

    • 有栈协程:协程会完整的做调用栈的保护、恢复工作,所以叫有栈协程
    • 无栈协程:本质上是一种有限状态机,状态保存在闭包里,比有栈协程更轻量,但是功能有限

    为什么需要协程?——因为“内核线程太贵了!”

    协程是一种用空间换时间、用用户态换内核态的聪明设计。

    你开了一家公司(程序),想让员工(线程)干活。但公司有个规定:每个员工都必须配一个独立办公室、工位、电脑、人事档案(内核资源)。这就像 Java 的 内核级线程(1:1 模型)

    问题来了:你想招 1万个 员工?不行!办公室不够,电脑买不起,人事系统也扛不住!

    而且每次换班(上下文切换),还得人事经理(操作系统内核)来登记、交接、签字,特别慢、特别累。所以大家想:能不能搞一种“轻量员工”?不用独立办公室,换班也不用找人事?

    这就是 协程(Coroutine) 的由来!

    什么是协程?——“灵活的临时工”

    协程 = 用户自己管理的“轻量线程”它不是操作系统管的,而是程序员自己在代码里调度的

    • 多个协程可以跑在同一个内核线程上(多对一模型)。
    • 它的调度由用户程序自己控制,不经过操作系统内核。
    • 切换快、开销小,能轻松创建成千上万个!
    类型内核线程协程
    身份正式员工,签合同,有工牌临时帮工,老板叫谁上谁上
    办公位独立工位(内核栈)共用一张桌子(用户空间)
    换班要人事登记(系统调用)老板一句话:“小王上,小李歇会儿”
    成本高(资源多)低(几乎不占系统资源)

    所以协程特别适合:高并发、大量 I/O(比如 Web 服务器处理上万请求)。

    协程是“协同式调度”——自己主动让位

    • 抢占式:时间一到,强制换人(Java 线程)
    • 协同式:自己干完活,说“我好了,你上吧”(协程)

    协程是协同式调度:它不会被系统强行中断,而是自己主动让出 CPU

    // Kotlin 协程例子
    launch {
        println("第一步")
        delay(1000) // 主动让出,不占用 CPU
        println("第二步")
    }

    比如在Kotlin中,delay(1000) 不是 sleep,它会主动挂起协程,让其他协程运行,1秒后再恢复。

    两种协程:有栈 vs 无栈

    有栈协程它像一个“完整的迷你线程”,有自己的独立记事本(调用栈)

    • 能在任何函数中暂停、恢复。
    • 支持复杂的调用链:A → B → C → 暂停 → 恢复 → D → E

    理解:

    你正在写代码,写到一半,老板说:“去接个电话。”你把当前所有变量、代码行、思路都记在私人笔记本上,合上本子去接电话。回来后,打开本子,接着写。这个“笔记本”就是它的私有栈

    缺点:

    • 每个协程都要分配栈内存(比如 1MB),虽然比线程小,但多了也吃不消。

    无栈协程没有独立的调用栈,状态保存在闭包(Closure)里,本质上是一个状态机

    • 不能在任意位置暂停,只能在 suspend 函数处挂起。
    • 状态用对象保存,比如“当前在第几步”。

    举个简单例子(JavaScript 风格):

    function* myCoroutine() {
        console.log("第一步");
        yield; // 暂停
        console.log("第二步");
    }

    它内部会被编译成:

    class StateMachine {
        int state = 0;
        void resume() {
            switch(state) {
                case 0:
                    System.out.println("第一步");
                    state = 1;
                    return;
                case 1:
                    System.out.println("第二步");
                    state = 2;
                    return;
            }
        }
    }

    看,它没有“栈”,只有 state 变量记录进度。

    优点:

    • 极其轻量!只用几个字节保存状态。
    • 创建百万个也不怕。

    缺点:

    • 功能受限,不能在任意函数中挂起。
    • 调试复杂(代码被编译成状态机)。

    有栈协程中有一种特例叫纤程,在新并发模型中,一段纤程的代码被分为两部分,执行过程和调度器:

    • 执行过程:用于维护执行现场,保护、恢复上下文状态
    • 调度器:负责编排所有要执行的代码顺序

     什么是纤程(Fiber)?——有栈协程的“升级版”

    纤程 = 有栈协程 + 更智能的调度器

    它把一段代码分成两部分:

    1. 执行过程(Execution)——“干活的人”

    • 负责维护执行现场:保存局部变量、调用栈、PC 寄存器等。
    • 就像一个“可暂停的函数执行器”。

    2. 调度器(Scheduler)——“包工头”

    • 负责安排哪个纤程先执行、什么时候切换。
    • 可以是单线程调度,也可以是多线程池调度。

    举个例子。你是个包工头(调度器),手下有一堆工人(纤程)。

    • 工人A在砌墙,砌到一半说:“我累了,歇会儿。”
    • 你记下他干到哪了(保存上下文),让他休息。
    • 让工人B去刷漆。
    • 过会儿,你再叫工人A回来,给他笔记本:“接着砌吧。”

    这就是纤程的“执行 + 调度”分离。

    线程状态

    进程的状态参考操作系统:创建态、就绪态、运行态、阻塞态、终止态

    线程由生到死的完整过程(生命周期):当线程被创建并启动以后,既不是一启动就进入了执行状态,也不是一直处于执行状态,在 API 中 java.lang.Thread.State 这个枚举中给出了六种线程状态:

    线程状态导致状态发生条件
    NEW(新建)线程刚被创建,但是并未启动,还没调用 start 方法,只有线程对象,没有线程特征
    Runnable(可运行)线程可以在 Java 虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器,调用了 t.start() 方法:就绪(经典叫法)
    Blocked(阻塞)当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入 Blocked 状态;当该线程持有锁时,该线程将变成 Runnable 状态
    Waiting(无限等待)一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入 Waiting 状态,进入这个状态后不能自动唤醒,必须等待另一个线程调用 notify 或者 notifyAll 方法才能唤醒
    Timed Waiting (限期等待)有几个方法有超时参数,调用将进入 Timed Waiting 状态,这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有 Thread.sleep 、Object.wait
    Teminated(结束)run 方法正常退出而死亡,或者因为没有捕获的异常终止了 run 方法而死亡

    对这张图的解释:

    • NEW → RUNNABLE:当调用 t.start() 方法时,由 NEW → RUNNABLE

    • RUNNABLE <--> WAITING:

      • 调用 obj.wait() 方法时

        调用 obj.notify()、obj.notifyAll()、t.interrupt():

        • 竞争锁成功,t 线程从 WAITING → RUNNABLE
        • 竞争锁失败,t 线程从 WAITING → BLOCKED
      • 当前线程调用 t.join() 方法,注意是当前线程在 t 线程对象的监视器上等待

      • 当前线程调用 LockSupport.park() 方法

    • RUNNABLE <--> TIMED_WAITING:调用 obj.wait(long n) 方法、当前线程调用 t.join(long n) 方法、当前线程调用 Thread.sleep(long n)

    • RUNNABLE <--> BLOCKED:t 线程用 synchronized(obj) 获取了对象锁时竞争失败

    也可以写一个简单的代码demo理解:

    public class ThreadStateDemo {
    
        // 共享锁对象,用于线程同步
        private static final Object lock = new Object();
    
        public static void main(String[] args) throws InterruptedException {
            System.out.println("程序开始:主线程启动");
    
            // 1. NEW → RUNNABLE:创建并启动线程
            Thread threadT1 = new Thread(() -> {
                synchronized (lock) {
                    System.out.println("线程T1:已进入同步块,当前状态为 RUNNABLE");
                    try {
                        System.out.println("线程T1:即将调用 wait(),进入 WAITING 状态...");
                        lock.wait(); // 释放锁,进入 WAITING
                        System.out.println("线程T1:被唤醒,重新获得锁,状态回到 RUNNABLE");
                    } catch (InterruptedException e) {
                        System.out.println("线程T1:等待期间被中断");
                    }
                }
            }, "Thread-T1");
    
            System.out.println("线程T1:刚创建,状态为 NEW");
            threadT1.start();
            System.out.println("线程T1:已调用 start(),状态变为 RUNNABLE");
    
            // 等待确保 t1 进入 WAITING
            Thread.sleep(1000);
    
            // 演示 TIMED_WAITING
            Thread threadT2 = new Thread(() -> {
                System.out.println("线程T2:开始执行,状态为 RUNNABLE");
                try {
                    System.out.println("线程T2:即将调用 sleep(3000),进入 TIMED_WAITING 状态...");
                    Thread.sleep(3000);
                    System.out.println("线程T2:sleep 结束,状态回到 RUNNABLE");
                } catch (InterruptedException e) {
                    System.out.println("线程T2:sleep 期间被中断");
                }
            }, "Thread-T2");
    
            threadT2.start();
    
            // 演示 BLOCKED 状态
            Thread threadT3 = new Thread(() -> {
                System.out.println("线程T3:尝试获取锁...");
                synchronized (lock) {
                    System.out.println("线程T3:成功获取锁,状态为 RUNNABLE(之前可能为 BLOCKED)");
                    System.out.println("线程T3:执行完毕");
                }
            }, "Thread-T3");
    
            threadT3.start(); // 此时 lock 被 threadT1 持有,t3 会 BLOCKED
    
            Thread.sleep(500); // 让 t3 尝试获取锁
    
            // 主线程唤醒 t1
            synchronized (lock) {
                System.out.println("主线程:准备唤醒在锁上等待的线程T1");
                lock.notify();
                System.out.println("主线程:已调用 notify()");
            }
    
            // 等待所有线程完成
            threadT1.join();
            threadT2.join();
            threadT3.join();
    
            System.out.println("所有线程均已执行完毕,状态为 TERMINATED");
            System.out.println("程序结束");
        }
    }
    转换说明
    NEW → RUNNABLE调用 start() 方法后,线程进入就绪状态,等待 CPU 调度
    RUNNABLE → WAITING调用 wait() 后,线程释放锁并无限等待,直到被 notify() 或 interrupt()
    WAITING → RUNNABLE被 notify() 唤醒后,尝试重新获取锁,成功后继续执行
    WAITING → BLOCKED被唤醒后,若其他线程持有锁,则进入 BLOCKED 状态等待锁
    RUNNABLE → TIMED_WAITING调用 sleep(n)wait(n)join(n) 等带超时的方法
    TIMED_WAITING → RUNNABLE超时结束或被提前中断
    RUNNABLE → BLOCKED尝试进入 synchronized 块/方法,但锁被其他线程持有
    BLOCKED → RUNNABLE成功获取锁后,进入运行状态

    查看线程

    Windows:

    • 任务管理器可以查看进程和线程数,也可以用来杀死进程
    • tasklist 查看进程
    • taskkill 杀死进程

    Linux:

    • ps -ef 查看所有进程
    • ps -fT -p 查看某个进程(PID)的所有线程
    • kill 杀死进程
    • top 按大写 H 切换是否显示线程
    • top -H -p 查看某个进程(PID)的所有线程

    Java:

    • jps 命令查看所有 Java 进程
    • jstack 查看某个 Java 进程(PID)的所有线程状态
    • jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)
    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值