本章内容

线程创建
线程重要 api,如 start,run,sleep,join,interrupt 等
线程状态
应用方面
异步调用:主线程执行期间,其它线程异步执行耗时操作
提高效率:并行计算,缩短运算时间
同步等待:join
统筹规划:合理使用线程,得到最优效果
原理方面
线程运行流程:栈、栈帧、上下文切换、程序计数器
Thread 两种创建方式 的源码
模式方面
终止模式之两阶段终止

1 创建和运行线程

方法一,直接使用 Thread

// 创建线程对象
Thread t = new Thread() {
 public void run() {
 // 要执行的任务
 }
};
// 启动线程
t.start();

例如:

// 构造方法的参数是给线程指定名字,推荐
Thread t1 = new Thread("t1") {
 @Override
 // run 方法内实现了要执行的任务
 public void run() {
 log.debug("hello");
 }
};
t1.start();

使用 Runnable 配合 Thread

把【线程】和【任务】(要执行的代码)分开
Thread 代表线程
Runnable 可运行的任务(线程要执行的代码)

Runnable runnable = new Runnable() {
 public void run(){
 // 要执行的任务
 }
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();

例如:

// 创建任务对象
Runnable task2 = new Runnable() {
 @Override
 public void run() {
 log.debug("hello");
 }
};
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();

输出

Java 8 以后可以使用 lambda 精简代码

// 创建任务对象
Runnable task2 = () -> log.debug("hello");
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();

FutureTask 配合 Thread

// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
 log.debug("hello");
 return 100;
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task3, "t3").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);

2 线程创建原理

 /**
     * Thread 构造阶段的核心初始化方法
     *
     * 作用:把一个 Thread 对象真正“配置好”,但此时线程还没启动(start)
     *
     * @param g                   线程组(ThreadGroup),为空则自动选择父线程组
     * @param target              线程要执行的任务(Runnable.run)
     * @param name                线程名称(不能为空)
     * @param stackSize           栈大小(JVM一般忽略,仅保留)
     * @param acc                 安全上下文(权限控制)
     * @param inheritThreadLocals 是否继承父线程的 InheritableThreadLocal
     */
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {

        // 线程名必须存在,否则直接抛异常
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        // 设置线程名称
        this.name = name;

        // parent = 当前正在创建新线程的线程(父线程) C层提供 与底层交互
        Thread parent = currentThread();

        // 获取系统安全管理器(用于权限检查)
        SecurityManager security = System.getSecurityManager();

        // 如果没有显式传入线程组 g,则自动决定线程组
        if (g == null) {

            // 如果存在 SecurityManager,则由安全管理器决定线程组
            if (security != null) {
                g = security.getThreadGroup();
            }

            // 如果安全管理器也没指定,则默认使用父线程的线程组
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

        // 无论 g 是否显式传入,都必须检查访问权限
        // 防止非法线程操作别人的线程组
        g.checkAccess();

        /*
         * 权限检查:防止用户随意继承/修改 ClassLoader 等敏感操作
         */
        if (security != null) {
            // 如果 Thread 类被子类重写了 contextClassLoader 方法
            // 则需要额外权限
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        // 在线程组中注册一个“未启动线程”
        // ThreadGroup 会统计线程数量
        g.addUnstarted();

        // 保存线程组引用
        this.group = g;

        // 默认继承父线程是否为守护线程(daemon)
        this.daemon = parent.isDaemon();

        // 默认继承父线程的优先级
        this.priority = parent.getPriority();

        // 设置上下文类加载器(ContextClassLoader)
        // 如果没有安全管理器,则直接继承父线程的
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;

        // 保存安全访问控制上下文(用于权限继承)
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();

        // 设置线程真正要执行的 Runnable 任务
        this.target = target;

        // 再次设置优先级(可能触发 JVM 校验范围 1~10)
        setPriority(priority);

        // 是否继承父线程的 InheritableThreadLocal
        // 用于父子线程之间传递上下文变量(如 TraceId)
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                    ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

        // 保存栈大小参数(一般 JVM 忽略,仅保留字段)
        this.stackSize = stackSize;

        // 分配线程唯一 ID(tid,全局递增)
        tid = nextThreadID();
    }

public synchronized void start() {
    /**
     * start() 是 Thread 启动线程的核心方法。
     *
     * 注意:这个方法不会用于 main 方法线程,也不会用于 JVM 内部的 system 线程组线程。
     * JVM 在创建这些线程时不会走 Java 层的 start()。
     *
     * 未来如果 start() 增加新功能,JVM 内部创建线程的逻辑也可能需要同步更新。
     *
     * threadStatus == 0 表示线程处于 NEW 状态(刚创建,还没启动)。
     */

    // 如果 threadStatus != 0,说明线程不是 NEW 状态
    // 线程已经启动过或者已经结束,不能重复 start()
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /**
     * 通知线程组(ThreadGroup):
     * 当前线程马上要启动了。
     *
     * group.add(this) 做的事情:
     * 1. 把该线程加入线程组的线程列表
     * 2. 线程组中“未启动线程数”减 1
     */
    group.add(this);

    // 标记线程是否成功启动
    boolean started = false;

    try {
        /**
         * start0() 是 native 方法(底层 C/C++ 实现)
         *
         * 真正创建操作系统线程,并让 JVM 调度执行 run()
         *
         * Java 层 start() 只是做检查和准备工作
         * 真正启动线程的是 start0()
         */
        start0();

        // 如果 start0() 没抛异常,说明启动成功
        started = true;
    } finally {
        /**
         * finally 保证:如果启动失败,也要做清理工作
         */
        try {
            if (!started) {
                /**
                 * 如果启动失败:
                 * 通知线程组:该线程启动失败
                 *
                 * 线程组需要回滚之前 add(this) 的操作:
                 * - 从线程组移除
                 * - unstarted count 恢复
                 */
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /**
             * 忽略异常:
             *
             * 如果 start0() 已经抛出了 Throwable,
             * 那么这个异常会继续向上抛出。
             *
             * 这里的清理异常不应该覆盖原始异常。
             */
        }
    }
}

init() 是 Thread 构造方法内部调用的初始化逻辑:

它主要完成:

  • 设置线程名

  • 设置优先级

  • 设置是否 daemon

  • 绑定 ThreadGroup

  • 复制父线程的上下文类加载器、ThreadLocal 等

它只是在“配置线程对象”,不是启动线程。

方法

是否创建线程 做什么
init() ❌ 否 初始化 Thread 对象属性
start() ❌ 否(只是入口) 检查状态 + 调用 start0
start0() ✅ 是 native 创建真正线程

3 查看进程线程的方法

平台 工具/命令 作用 常用示例/说明
Windows 任务管理器 查看进程和线程数,结束进程 图形界面操作,可直接杀死进程
Windows tasklist 查看当前系统所有进程 tasklist
Windows taskkill 杀死指定进程 taskkill /PID 1234 /F
Linux ps -fe 查看所有进程 ps -fe
Linux ps -fT -p <PID> 查看某进程下的所有线程 ps -fT -p 1234
Linux kill 杀死进程(发送信号) kill -9 1234
Linux top 动态查看进程资源占用 H 切换是否显示线程
Linux top -H -p <PID> 查看某进程下所有线程动态情况 top -H -p 1234
Java jps 查看所有 Java 进程 显示 Java 程序 PID 和名称
Java jstack <PID> 查看某 Java 进程的线程栈和状态 jstack 1234
Java jconsole 图形界面监控线程、内存、CPU等 可查看线程运行情况

jconsole 远程监控配置知识点

步骤 配置项 说明
1 启动时添加 JMX 参数 必须用特殊方式启动 Java 程序开启远程监控
2 修改 /etc/hosts 127.0.0.1 映射到主机名,避免连接问题
3 开启认证访问(可选) 需要用户名密码才能连接
4 复制 jmxremote.password 文件 提供用户密码配置
5 修改权限为 600 chmod 600 jmxremote.password jmxremote.access
6 登录信息 用户名:controlRole 密码:R&D

4 原理之线程运行

层级 概念 对象归属 说明 关键词
第 1 层:线程级 Java 虚拟机栈(JVM Stack) 线程私有 每个线程启动后 JVM 都会分配一块独立栈内存 一个线程一个栈
第 2 层:方法级 栈帧(Frame) 方法调用私有 每次方法调用都会创建一个栈帧,方法结束则出栈 一次调用一个栈帧
第 3 层:执行级 活动栈帧 当前方法 一个线程同一时刻只有一个栈帧处于活动状态(栈顶) 栈顶正在执行
第 4 层:数据级 栈帧内部结构 方法运行数据 局部变量表、操作数栈、返回地址、动态链接等信息都在栈帧中 方法执行上下文
层级 概念 触发原因 保存内容 性能影响
第 1 层:现象 Context Switch CPU 从一个线程切到另一个线程 OS 负责调度切换 切换有成本
第 2 层:触发 常见原因 时间片用完、GC、更高优先级线程 当前线程暂停执行 线程越多越频繁
第 3 层:主动行为 阻塞/让出 CPU 方法 sleep yield wait join park synchronized lock 线程进入等待或阻塞 增加切换次数
第 4 层:保存现场 线程状态恢复机制 切换时必须保存执行位置 PC 寄存器 + 栈帧信息(局部变量、操作数栈、返回地址) 恢复执行依赖这些数据

线程 → 栈 → 栈帧 → 活动栈帧 → 局部变量/操作数栈/返回地址

5 常见方法

方法名 static 功能说明 注意事项
start() 启动一个新线程,在新线程中执行 run() 方法代码 start() 只是让线程进入 就绪态,不一定立刻运行(CPU 时间片未分配)。同一个线程对象只能调用一次,多次调用抛 IllegalThreadStateException
run() 新线程启动后执行的方法入口 如果构造 Thread 时传入 Runnable,则执行 Runnable 的 run();否则默认空执行。也可继承 Thread 重写 run()
join() 等待线程执行结束 当前线程阻塞等待目标线程结束
join(long n) 最多等待 n 毫秒,超时后继续执行 属于 TIMED_WAITING 状态
getId() 获取线程唯一的 long 型 id id 全局唯一
getName() 获取线程名称 便于调试与日志定位
setName(String) 修改线程名称 建议业务线程命名清晰
getPriority() 获取线程优先级 默认 5
setPriority(int) 修改线程优先级 优先级范围 1~10,值越大调度概率越高,但不保证绝对先执行
getState() 获取线程状态 6 种状态:NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED
isInterrupted() 判断线程是否被打断 不会清除打断标记
isAlive() 判断线程是否存活(未执行完) 启动后到结束前返回 true
interrupt() 打断线程 若线程在 sleep/wait/join 中会抛 InterruptedException 并清除标记;若线程运行中则仅设置打断标记;park 被打断也会设置标记
interrupted() 判断当前线程是否被打断 会清除打断标记,只作用于当前线程
currentThread() 获取当前正在执行的线程对象 常用于日志、调试、获取线程上下文
sleep(long n) 当前线程休眠 n 毫秒,并让出 CPU 时间片 不释放锁;进入 TIMED_WAITING
yield() 提示调度器让出 CPU 使用权 主要用于测试调试,不保证一定让出
  • 启动线程:start()(只能一次)

  • 执行入口:run()

  • 等待结束:join()

  • 打断机制:interrupt / isInterrupted / interrupted

  • 静态方法:currentThread / sleep / yield / interrupted

start 与 run
public class Main {
    public static void main(String[] args) {
        //TIP 当文本光标位于高亮显示的文本处时按 <shortcut actionId="ShowIntentionActions"/>
        // 查看 IntelliJ IDEA 建议如何修正。
        Thread t1 = new Thread("t1") {
            @Override
            // run 方法内实现了要执行的任务
            public void run() {
                System.out.println("t1");
            }
        };
        t1.run();
    }
}

调用 t1.run():这只是一个普通的方法调用,并不会创建新线程,run() 方法里的代码会在主线程(main 线程) 中执行。
调用 t1.start():这是启动线程的正确方式,JVM 会创建一个名为 t1 的新线程,run() 方法里的代码会在新线程中执行。

sleep 与 yield
对比项 sleep() yield() 线程优先级(priority)
核心作用 让线程主动休眠一段时间 让线程主动让出CPU一次 提示调度器更倾向调度某线程
状态变化 Running → Timed Waiting(阻塞) Running → Runnable(就绪) 不直接改变状态,只影响调度倾向
是否阻塞线程 是(明确阻塞) 否(只是让出时间片)
是否释放锁 不释放(sleep期间仍持有锁) 不释放 不涉及锁释放
唤醒方式 时间到自动恢复,可被 interrupt() 打断 立刻回到就绪队列等待调度 依赖调度器分配时间片
是否抛异常 会抛 InterruptedException 不会抛异常 不会抛异常
调度结果确定性 相对确定:至少睡够时间 不确定:可能马上又调度回来 不确定:调度器可忽略
典型用途 延迟、定时任务、控制节奏 优化竞争、减少独占CPU 线程任务重要性提示
可读性建议 推荐 TimeUnit.SECONDS.sleep() 无特别替代 不建议依赖优先级保证顺序
join 方法详解
static int r = 0;
public static void main(String[] args) throws InterruptedException {
 test1();
}
private static void test1() throws InterruptedException {
 log.debug("开始");
 Thread t1 = new Thread(() -> {
 log.debug("开始");
 sleep(1);
 log.debug("结束");
 r = 10;
 });
 t1.start();
 log.debug("结果为:{}", r);
 log.debug("结束");
}

因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 r=10
而主线程一开始就要打印 r 的结果,所以只能打印出 r=0
解决方法
用 sleep 行不行?为什么?

sleep 不是线程同步手段,只是时间延迟
用 join,加在 t1.start() 之后即可

应用之同步

以调用方角度来讲,如果

需要等待结果返回,才能继续运行就是同步
不需要等待结果返回,就能继续运行就是异步

t r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
 test2();
}
private static void test2() throws InterruptedException {
 Thread t1 = new Thread(() -> {
 sleep(1);
 r1 = 10;
 });
 Thread t2 = new Thread(() -> {
 sleep(2);
 r2 = 20;
});
 long start = System.currentTimeMillis();
 t1.start();
 t2.start();
 t1.join();
 t2.join();
 long end = System.currentTimeMillis();
 log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}

分析如下
第一个 join:等待 t1 时, t2 并没有停止, 而在运行
第二个 join:1s 后, 执行到此, t2 也运行了 1s, 因此也只需再等待 1s
如果颠倒两个 join 呢?
最终都是输出

有时效的 join

等够时间

static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
 test3();
}
public static void test3() throws InterruptedException {
 Thread t1 = new Thread(() -> {
 sleep(2);
 r1 = 10;
 });
 long start = System.currentTimeMillis();
 t1.start();
 // 线程执行结束会导致 join 结束
 t1.join(1500);
 long end = System.currentTimeMillis();
 log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
interrupt 方法详解

打断 sleep,wait,join 的线程
这几个方法都会让线程进入阻塞状态
打断 sleep 的线程, 会清空打断状态,以 sleep 为例
 // 线程执行结束会导致 join 结束
 

    private static void test2() throws InterruptedException {
        Thread t2 = new Thread(()->{
            while(true) {
                Thread current = Thread.currentThread();
                boolean interrupted = current.isInterrupted();
                if(interrupted) {
                 
                    System.out.println(interrupted);
                    break;
                }
            }
        }, "t2");
        t2.start();
        sleep(1);
        t2.interrupt();
    }

java.lang.InterruptedException: sleep interrupted
 at java.lang.Thread.sleep(Native Method)
 at java.lang.Thread.sleep(Thread.java:340)
 at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
 at cn.itcast.n2.util.Sleeper.sleep(Sleeper.java:8)
 at cn.itcast.n4.TestInterrupt.lambda$test1$3(TestInterrupt.java:59)
 at java.lang.Thread.run(Thread.java:745)
21:18:10.374 [main] c.TestInterrupt - 打断状态: false

打断正常运行的线程, 不会清空打断状态

可以做线程循环控制

类似于信号机制

/**
 * 两阶段终止模式
 * 第一阶段:发出停止信号(interrupt)
 * 第二阶段:线程收尾工作后退出
 */

package org.example;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class TwoPhaseTerminationDemo {

    public static void main(String[] args) throws InterruptedException {
        Monitor monitor = new Monitor();
        monitor.start();

        Thread.sleep(3000);

        monitor.stop();
    }
}

/**
 * 两阶段终止模式
 * 第一阶段:发出停止信号(interrupt)
 * 第二阶段:线程收尾工作后退出
 */
@Slf4j
class Monitor {

    private Thread monitorThread;

    public void start() {
        monitorThread = new Thread(() -> {
            while (true) {
                // 第二阶段:检测停止信号
                if (Thread.currentThread().isInterrupted()) {
                    log.debug("收到停止信号,开始收尾工作...");
                    break;
                }

                try {
                    log.debug("执行监控任务...");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // sleep 被打断会清除 interrupt 标记,需要重新设置
                    Thread.currentThread().interrupt();
                }
            }

            log.debug("收尾完成,线程终止");
        });

        monitorThread.start();
    }

    public void stop() {
        log.debug("发出停止信号...");
        monitorThread.interrupt();
    }
}


18:35:58.951 [Thread-1] DEBUG org.example.Monitor - 执行监控任务...
18:35:59.958 [Thread-1] DEBUG org.example.Monitor - 执行监控任务...
18:36:00.965 [Thread-1] DEBUG org.example.Monitor - 执行监控任务...
18:36:01.956 [main] DEBUG org.example.Monitor - 发出停止信号...
18:36:01.956 [Thread-1] DEBUG org.example.Monitor - 收到停止信号,开始收尾工作...
18:36:01.956 [Thread-1] DEBUG org.example.Monitor - 收尾完成,线程终止

进程已结束,退出代码为 0

打断 park 线程

打断 park 线程, 不会清空打断状态

private static void test3() throws InterruptedException {
 Thread t1 = new Thread(() -> {
 log.debug("park...");
 LockSupport.park();
 log.debug("unpark...");
 log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
 }, "t1");
 t1.start();
 sleep(0.5);
 t1.interrupt();
}

21:11:52.795 [t1] c.TestInterrupt - park... 
21:11:53.295 [t1] c.TestInterrupt - unpark... 
21:11:53.295 [t1] c.TestInterrupt - 打断状态:true 
如果打断标记已经是 true, 则 park 会失效

private static void test4() {
 Thread t1 = new Thread(() -> {
 for (int i = 0; i < 5; i++) {
 log.debug("park...");
 LockSupport.park();
 log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
 }
 });
 t1.start();
 sleep(1);
 t1.interrupt();
}

21:13:48.783 [Thread-0] c.TestInterrupt - park... 

等待打断通知
21:13:49.809 [Thread-0] c.TestInterrupt - 打断状态:true 
21:13:49.812 [Thread-0] c.TestInterrupt - park... 
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true 
21:13:49.813 [Thread-0] c.TestInterrupt - park... 
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true 
21:13:49.813 [Thread-0] c.TestInterrupt - park... 
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true 
21:13:49.813 [Thread-0] c.TestInterrupt - park... 
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true 

提示
可以使用 Thread.interrupted() 清除打断状态

6 不推荐的方法

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

方法名 static 功能说明
stop() 停止线程运行(强制终止线程,已废弃,不安全)
suspend() 挂起(暂停)线程运行(已废弃,可能导致死锁)
resume() 恢复线程运行(已废弃,依赖 suspend,存在风险)

7 主线程与守护线程

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守
护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

log.debug("开始运行...");
Thread t1 = new Thread(() -> {
 log.debug("开始运行...");
 sleep(2);
 log.debug("运行结束...");
}, "daemon");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
sleep(1);
log.debug("运行结束...");

08:26:38.123 [main] c.TestDaemon - 开始运行... 
08:26:38.213 [daemon] c.TestDaemon - 开始运行... 
08:26:39.215 [main] c.TestDaemon - 运行结束...

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

8 五种状态

这是从 操作系统 层面来描述的

状态名称 对应代码示例 含义说明 状态转换特点
初始状态(New) Thread t = new Thread(() -> {}); 仅在语言层面创建了线程对象,还未与操作系统线程关联 调用 start() 后进入可运行状态(等待调度)
可运行状态(Runnable,就绪) t.start(); 线程已创建并与操作系统线程关联,可以等待 CPU 调度执行 被 CPU 调度后进入运行状态
运行状态(Running) run() 方法中的代码正在执行 获取 CPU 时间片,正在执行线程代码 时间片用完会回到可运行状态(发生上下文切换)
阻塞状态(Blocked/Waiting) Thread.sleep(1000); / socket.read(); 调用了阻塞 API(如 BIO 文件读写),线程不占用 CPU 阻塞结束后由操作系统唤醒,回到可运行状态
终止状态(Terminated) run() 执行结束 线程执行完毕,生命周期结束 不会再转换到其他状态

9 六种状态

这是从 Java API 层面来描述的
根据 Thread.State 枚举,分为六种状态

  • NEW:线程对象刚创建出来,还没调用 start(),只是“准备好了”,并未真正启动。

  • RUNNABLE:调用了 start() 之后进入该状态。
    注意:在 Java 中,RUNNABLE 是一个“大状态”,它包含了操作系统层面的:

    • 就绪(等待 CPU)

    • 运行(正在执行)

    • 因 BIO 阻塞(如文件读写卡住)
      Java API 无法细分这些情况,所以统一都算 RUNNABLE

  • BLOCKED / WAITING / TIMED_WAITING:这些是 Java 对“阻塞状态”的进一步细分:

    • BLOCKED:等待锁

    • WAITING:无限期等待(如 wait()join()

    • TIMED_WAITING:限时等待(如 sleep()、带超时的 wait()

  • TERMINATED:线程的 run() 方法执行完毕,线程生命周期结束,不会再运行。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐