【Java多线程(2)】-- Thread的常见属性和状态、interrupt()方法和join()方法
本文详细介绍了Java线程的等待、中断与状态管理机制。主要内容包括:1.线程等待方法join()的使用,包括无限期等待和限时等待;2.线程常见属性如ID、名称、状态等的获取方法;3.前台线程与后台线程的区别及设置方式;4.线程中断的两种实现方式(自定义标志位和interrupt()方法);5.线程的6种状态(NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TE
目录
1. 等待一个线程
1.1 主动等待: join()
join() 是 Java Thread 类的一个关键方法,用于线程同步。它让一个线程等待另一个线程执行完毕后再继续执行。
- 被等待的线程:如果一个线程对象调用了join()方法,那么该线程对象就是被等待的线程。【被别人等,自己正常执行】
- 例如: “thread1.join()”,在该语句中是thread1调用了join()方法,所以thread1是被等待的线程。
- 等待的线程:调用" 线程对象.join() "语句 的那个线程。【主动暂停自己,等别人】
- 例如:在main方法中执行了“thread1.join()”语句,此时主线程是等待的线程。相当于把thread1的任务加入到主线程的线性执行中。
-- 例1:main线程等待 t 线程,t 线程打印5次“hello thread”后,main线程继续
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for(int i = 0; i < 5; i++){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
});
t.start();
System.out.println("主线程等待之前");
t.join(); // join会触发阻塞,可能会抛出 InterruptedException 异常
System.out.println("主线程等待之后");
}
打印5次“hello thread”后,t 线程结束。这意味着main线程的等待结束,继续main线程并打印“主线程等待之后”:

-- 例2: t 线程等待main线程,main线程打印3次“hello main”后,t 线程继续
补充:currentThread()方法可以获取当前运行的线程
public static void main(String[] args) throws InterruptedException {
//获取main线程的引用
Thread mainThread = Thread.currentThread();
Thread t = new Thread(() -> {
//让t线程阻塞等待main线程
try {
System.out.println("t线程等待前");
mainThread.join();
System.out.println("t线程等待后");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
//等main线程做完下面的循环,t线程才会继续执行
for (int i = 0; i < 3; i++){
System.out.println("hello main");
Thread.sleep(1000);
}
}
打印3次“hello main”后,主线程结束。这意味着 t 线程的等待结束,于是继续 t 线程并打印 “ t 线程等待后”:

1.2 有时限地等待
join()方法不仅只有一种,它还有其他重写版本:
| 方法 | 说明 |
| void join() | 无限期等待,当前线程一直等待,直到目标线程执行完毕才继续。 |
| void join(long millis) | 限时等待,当前线程最多等待 millis 毫秒,超时后无论目标线程是否结束,当前线程都会继续执行。 |
| void join(long millis, int nanos) | 更精准的限时等待,在毫秒基础上增加纳秒级精度(nanos 范围 0~999999)。 |
无参的join()会无期限地等待,倘若被等待线程中的任务逻辑有错误会造成死循环,那么等待线程会因为无限期等待而造成死等。
所以我们常常会使用限时等待版本的join(long millis)。
不过 join(long millis, int nanos) 几乎没人用,因为它的纳秒级精度在实际场景中完全无法落地。详细原因可以归咎于以下几点:
- 操作系统的时间精度:线程的等待/调度是由操作系统内核负责的,即使是高性能服务器,系统时钟的精度也远达不到纳秒级别。
- JDK 的降级处理:从 JDK 源码来看,join(long millis, int nanos) 并没有真正处理纳秒级等待,而是将纳秒参数做了“四舍五入到毫秒”的处理,本质还是依赖 join(long millis)。
- 业务场景不需要:实际开发中只需要「毫秒级」的等待控制就足够了。
- 使用成本更高:硬件层面和代码层面都需要做额外的升级和维护,成本远高于收益。
-- 例1:join()的死等
public static void main(String[] args) throws InterruptedException {
Thread main = Thread.currentThread();
System.out.println("开始等待自己");
long begin = System.currentTimeMillis();
main.join();
long end = System.currentTimeMillis();
System.out.println("等待结束, 共等待 " + (end - begin) + "ms");
}
当主线程结束后,join()方法才会结束。可是主线程因为“main.join()”语句在,又无法结束,最终造成死等:

打印“开始等待自己”后进入了死等状态,什么也没做。
-- 例2:有期限的等待
public static void main(String[] args) throws InterruptedException {
Thread main = Thread.currentThread();
System.out.println("开始等待自己");
long begin = System.currentTimeMillis();
main.join(1000);
long end = System.currentTimeMillis();
System.out.println("等待结束, 共等待 " + (end - begin) + "ms");
}
等待大概1秒后,主线程停止等待,并把后面的内容打印了出来:

可以看到,虽然说是等待1000ms,可最终是等了1006ms,可见操作系统和JDK根本没有能力处理 "纳秒级" 的线程调度。
1.3 join方法 与 sleep方法 的区别
join() 与 sleep()这两个方法都涉及线程等待,但设计目的和行为有本质区别。
| 对比维度 | join方法 | sleep方法 |
| 作用、目的 | 让 当前线程 等待 目标线程 执行完毕,再继续自身执行。可以实现简单的线程同步。 | 让当前线程休眠指定时间,时间到后自动唤醒。不涉及线程的同步。 |
| 方法类型 | 由当前线程的实例对象调用的实例方法。 | Thread类中的静态方法。 |
| 重载种类 | 共3种,可以无期限地等待,也可以有期限地等待。 | 共2种,只支持有期限地等待。 |
| 线程状态 |
无参方法:WAITING; 有参方法: TIMED_WAITING。 |
TIMED_WAITING(sleep 只有限时等待版本) |
| 锁行为 | 底层实现依赖 Object.wait(),会获取和释放对象锁。 | 不涉及到锁行为。 |
2. Thread的常见属性
Thread 类提供了丰富的属性用于管理和监控线程。以下是重要的线程属性及其获取方法:
| 属性 | 获取方法 |
| ID | getId() |
| 名称 | getName() |
| 状态 | getState() |
| 优先级 | getPriority() |
| 是否为后台线程 | isDaemon() |
|
是否存活 |
isAlive() |
| 是否被中断 | isInterrupted() |
2.1 获取线程的名称
如果创建Thread对象时有命名,那么线程名就是创建时起的名字。如果创建Thread对象时没有命名,那么创建出来的线程采用默认的命名方式,从"Thread-0"开始依次命名,且该序号与总线程数无关,与使用无参构造方法创建出来的Thread对象个数有关。
-- 例如:
public static void main(String[] args) {
Thread t1 = new Thread();
Thread t2 = new Thread("线程t2");
Thread t3 = new Thread();
System.out.println(t1.getName());
System.out.println(t2.getName());
System.out.println(t3.getName());
}

t3线程的名称并不是“Thread-2”,因为使用无参构造方法创建出来的Thread对象个数只有2个,所以它的名称为“Thread-1”。
2.2 检查线程是否存活
线程只在两种情况下会处于 "非存活" 状态,一个是线程还没开启,另一个是线程已经结束。
无论线程是否处于 阻塞 或 等待(如sleep、join) 状态,它都是"存活"的。
线程状态 是否存活( isAlive())核心说明 NEW(新建)❌ false 仅创建 Thread对象,未start()RUNNABLE(可运行)✅ true 已启动,正在 / 等待 CPU 调度 BLOCKED(阻塞)✅ true 等待 synchronized锁,仍存活WAITING(无限等待)✅ true 等待唤醒,仍存活 TIMED_WAITING(限期等待)✅ true 限时等待,仍存活 TERMINATED(终止)❌ false run()执行完 / 异常终止
-- 例:thread会花3秒时间打印“hello thread”,我们观察thread线程前后的存活状态
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
for(int i = 0; i < 3; i++){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
System.out.println("thread线程未启动, 是否存活:" + thread.isAlive());
thread.start();
System.out.println("thread线程已启动, 是否存活:" + thread.isAlive());
Thread.sleep(4000); //打印需要3秒, 休眠了4秒, 唤醒后thread线程已结束
System.out.println("thread线程已结束, 是否存活:" + thread.isAlive());
}

2.3 后台线程与前台线程的区别
计算机术语中的后台线程,可不是日常生活中说的那种“你没盯着它,但它还在干活”的线程。"后台线程"在口语和术语中是完全不同的意思,下面是后台线程的定义:
前台线程 (也叫用户线程):如果该类型的线程没运行完毕,那么进程就不能结束。【线程能够阻止进程的结束】
后台线程 (也叫守护线程):当进程要结束时,该类型的线程无论是否运行完毕,它都会被强制结束且不会保留上下文。【线程不能阻止进程的结束】
在Java (JVM进程) 中,所有线程默认都是前台线程。如果要修改线程为后台线程,需要用到setDaemon()方法。
- 目标线程.setDaemon(false):设置目标线程为前台线程。
- 目标线程.setDaemon(true):设置目标线程为后台线程。
- 注意:setDaemon()方法只能在start()方法调用之前使用!!!
-- 例1:java线程默认是前台线程
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "我的线程");
//是否为后台线程与线程是否运行无关
//1.默认是前台线程
System.out.println("是否是后台线程:" + thread.isDaemon());
//2.修改后
thread.setDaemon(true);
System.out.println("是否是后台线程:" + thread.isDaemon());
}

例2:main方法结束后,JVM会检查是否还有前台线程,如果没有则关闭进程。
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "我的线程");
//【注意,setDaemon方法只能在start方法之前使用
thread.setDaemon(true);
thread.start();
System.out.println("是否是后台线程:" + thread.isDaemon());
//thread为后台线程时,main方法一结束,JVM会发现没有要管理的前台线程,那么thread也要跟着结束
for(int i = 0; i < 3; i++){
System.out.println("hello main");
Thread.sleep(1000);
}
}
打印3次“hello main”后进程退出,后台线程thread也跟着结束,不再打印“hello thread”

进程退出是需要时间的,这里刚好有时间给thread线程多打印一次“hello thread”。
3. 中断一个线程
Java 中的中断线程并非 "强行杀死线程",而是一种线程间的协作通信机制。它的本质是向目标线程发送 "你可以停止运行了" 的协作信号,而非强制终止指令 —— 线程是否停止、何时停止,完全由目标线程自身的业务逻辑决定,外部无法强行干预。
中断标志位是接收和传递这一信号的核心载体,通常分为两类:
- 自定义中断标志位:开发者自行定义的
volatile布尔变量(需加volatile保证多线程可见性),通过修改变量值传递中断信号。- JVM 内置中断标志位:Thread类的私有布尔属性,由 JVM 维护。当外部线程调用 "
interrupt()" 时,JVM 会将该标志位设为true。
- 注意:若线程在
sleep()、wait()、join()等自身已经是阻塞状态下被中断,会触发InterruptedException,且 JVM 会自动清除该标志位,继而从阻塞状态转为运行状态。
3.1 自定义中断标志位
我们可以使用自定义的布尔变量作为中断信号。
-- 例:t 线程每隔一秒打印一次“hello thread”,main线程在3秒后修改中断标志位flag。修改后 t 线程的while循环检测到flag修改于是结束while循环。
【volatile 的作用是确保变量的修改对所有线程立即可见,避免线程缓存导致的不一致。
【此代码中即使不加volatile修饰变量flag,也不会影响最后的结果,因为 sleep 1秒的时间太长,不会触发编译器优化。
public class Demo {
private static volatile boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!flag){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
//让main线程终止 t 线程
System.out.println("3秒后触发中断标志位...");
Thread.sleep(3000);
flag = true;
System.out.println("触发中断标志位, t 线程终止..");
}
}

3.2 使用interrupt()方法中断线程
Thread类中自带私有的中断标志位,我们可以通过判断该字段来中断线程。
-- 例:t 线程会不断地打印“hello thread”。10毫秒后main线程会调用 t 线程的interrupt()方法,while循环检测到中断信号后终止循环。
public class Demo {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()){
System.out.println("hello thread");
}
});
t.start();
//10ms后线程中断
Thread.sleep(10);
t.interrupt();
}
}
虽然只是10ms,但是已经够连续打印了几十次“hello thread”了:

#1 currentThread()方法
刚刚的例子中,我们的while循环使用 “Thread.currentThread().isInterrupted()” 作为判断条件。那为什么不使用 “t.isInterrupted()” 作为判断条件呢?
原因 1:lambda 作用域与 Thread 类的成员结构限制
- lambda表达式作为Thread构造方法中一个参数,它为Thread类的target私有字段提供了一个重写了run()方法的Runnable对象。
- 而Thread类中并没有名称为 t 的成员字段,因此在 run() 方法的上下文里,无法直接依赖外部(Demo类中的main方法)的 t 变量。故run()方法中不能使用 “t.isInterrupted()”。
原因 2:Java 语法特性
- currentThread()是Thread类中的静态方法,它可以返回当前线程的引用。
- 当程序运行到“while (!Thread.currentThread().isInterrupted())”时,t 线程早已被创建出来,于是currentThread()方法可以顺利返回 t 线程的引用。
- 而run()方法是实例方法,实例方法可以调用静态方法和静态变量,这是Java语法的特性。
currentThread()的返回规则:
Thread.currentThread()总是返回当前正在执行这行代码的线程对象的引用。比如在主线程执行 “Thread.currentThread()” 就是获取主线程的引用,在thread1线程中执行 “Thread.currentThread()” 就是获取thread1线程的引用。
#2. 标志位的异常重置
前面说过,如果线程在 sleep()、wait()、join()方法执行期间调用interrupt()方法,那么线程会从阻塞状态转为运行状态。可能很多人会误以为interrupt()方法会无脑地对标志位取反,但现实是完全相反的!
interrupt()方法并不会对标志位取反,反而是一定会把标志位设为true。
我们从程序执行顺序来找出问题所在:
- 调用 sleep()/wait()/join() 方法:标志位被设为true。
- 调用interrupt() 方法:
- 它会先将标志位设为 true。
- 若线程正处于 sleep()/wait()/join() 阻塞,interrupt()会将它们提前唤醒。这种唤醒属于异常行为,会抛出 InterruptedException。
- 无论线程是提前唤醒还是自动唤醒,JVM都会自动将标志位重置为false。
我用代码演示一下:
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("sleep方法被提前唤醒...");
}
}
});
t.start();
//主线程在两秒后尝试中断 t 线程
Thread.sleep(2000);
t.interrupt();
}

抛出异常后我们的异常处理仅仅是打印异常原因,打印后 t 线程继续执行。所以这里打印异常后继续打印“hello thread”。
我们修改一下异常处理的方式,使得这段代码满足“使用interrupt()方法后中断线程,线程不在执行任何事情”的逻辑:
把“打印”异常改成“跳出循环”即可
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//中断线程应该让线程什么事情都不再做,所以我们要跳出循环
break;
}
}
});
t.start();
Thread.sleep(2000);
t.interrupt();
}

这次只打印2次“hello thread”后,t 线程就真正终止了。
4. 线程的状态
4.1 线程的所有状态
线程的状态是⼀个枚举类型Thread.State
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}

详细说明:
| 状态枚举值 | 核心含义 | 触发条件(关键操作) |
|---|---|---|
|
(新建) |
创建 Thread 对象,但未调用 start(),线程未真正启动 |
Thread t = new Thread(...) 后,未执行 t.start() |
|
|
线程已启动(调用 ① 正在 CPU 执行(Running) ② 等待 CPU 调度,线程此时还没正式开始执行(Ready) |
1. 2. 阻塞 / 等待状态恢复后回到 RUNNABLE状态 |
|
|
线程因争抢 synchronized 内置锁 失败而阻塞(仅针对 synchronized) | 进入 synchronized 方法 / 代码块,但锁被其他线程持有 |
|
|
线程无限期等待,需其他线程主动唤醒(无超时),否则永久等待 |
调用无超时参数的方法: 1. 2. 3. |
TIMED_WAITING(限期等待) |
线程有限期等待,超时自动唤醒,也可被提前唤醒 |
调用带超时参数的方法: 1. 2. 3. 4. |
|
(终止) |
线程生命周期结束:run() 正常执行完,或因未捕获异常终止 |
1. 2. 线程抛出未捕获的 RuntimeException / Error |
- Java 线程的 6 种状态是互斥且全覆盖的,一个 Java 线程在任意时刻只能属于 Thread.State 枚举中的唯一一种状态。
- 狭义下的阻塞只有BLOCKED,它是官方指定的唯一 “阻塞状态”。广义下的阻塞包括BLOCKED、WAITING、TIMED_WAITING。
4.2 状态演示
-- 例1:NEW状态与TERMINATED状态的演示
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("hello thread");
});
//1.线程还未开启(new)
System.out.println("当前状态:" + t.getState() + " 线程是否存活:" + t.isAlive());
t.start();
//2.线程已完成工作(terminated)
Thread.sleep(1000); //等待一会确保 t 线程完全结束
System.out.println("当前状态:" + t.getState() + " 线程是否存活:" + t.isAlive());
}
strat()方法没被调用时,第一个打印处于NEW状态。当 t 线程结束后,第二个打印处于TERMINATED状态:

-- 例2:RUNNABLE、WAITING、TIMED_WAITING状态的演示
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (true){
try {
// 3.对于 t 线程,执行sleep是time_waiting状态
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
// 4.RUNNABLE 表示线程可以运行:包括运行状态(running)和就绪状态(ready)
System.out.println(t.getState() + " 线程是否存活:" + t.isAlive());
// 5.对于main线程,无参的join会导致主线程是 waiting 状态
t.join();
}
t 线程的任务是死循环,一旦开启了 t 线程就不会停下。所以在start()方法调用后我们可以得到RUNNABLE状态:

通过jconsole监视线程 t ,由于 t 线程执行的是有期限的sleep(1000)方法,所以 t 线程处于TIMED_WAITING状态:

通过jconsole监视线程主线程,由于主线程执行的是无限期的join()死等方法,所以主线程处于WAITING状态:

-- 例3:BLOCKED状态的演示
public class Test{
private static Object locker = new Object();
public static void main(String[] args) {
Thread t = new Thread(() -> {
synchronized (locker){
System.out.println("I am thread");
while (true){
}
}
});
t.start();
synchronized (locker){
System.out.println("I am main");
while (true){
}
}
}
}
谁拿到了锁,谁就会打印

这里是main线程拿到了锁,我们通过jconsole可以观察到:main线程处于RUNNABLE状态,t 线程处于BLOCKING阻塞状态:


本期分享完毕,感谢大家的支持Thanks♪(・ω・)ノ

更多推荐


所有评论(0)