目录

1. 等待一个线程

1.1 主动等待: join()

1.2 有时限地等待

1.3 join方法 与 sleep方法 的区别

2. Thread的常见属性

2.1 获取线程的名称

2.2 检查线程是否存活

2.3 后台线程与前台线程的区别

3. 中断一个线程

3.1 自定义中断标志位

3.2 使用interrupt()方法中断线程

#1 currentThread()方法

#2. 标志位的异常重置

4. 线程的状态

4.1 线程的所有状态

4.2 状态演示


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 检查线程是否存活

线程只在两种情况下会处于 "非存活" 状态,一个是线程还没开启,另一个是线程已经结束

线程状态 是否存活(isAlive() 核心说明
NEW(新建) ❌ false 仅创建 Thread 对象,未 start()
RUNNABLE(可运行) ✅ true 已启动,正在 / 等待 CPU 调度
BLOCKED(阻塞) ✅ true 等待 synchronized 锁,仍存活
WAITING(无限等待) ✅ true 等待唤醒,仍存活
TIMED_WAITING(限期等待) ✅ true 限时等待,仍存活
TERMINATED(终止) ❌ false run() 执行完 / 异常终止
无论线程是否处于 阻塞 或 等待(如sleep、join) 状态,它都是"存活"的。

-- 例: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

我们从程序执行顺序来找出问题所在:

  1. 调用 sleep()/wait()/join() 方法:标志位被设为true。
  2. 调用interrupt() 方法:
    1. 它会先将标志位设为 true。
    2. 若线程正处于 sleep()/wait()/join() 阻塞,interrupt()会将它们提前唤醒。这种唤醒属于异常行为,会抛出 InterruptedException
  3. 无论线程是提前唤醒还是自动唤醒,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);
    }
}

详细说明:

状态枚举值 核心含义 触发条件(关键操作)

NEW

(新建)

创建 Thread 对象,但未调用 start(),线程未真正启动 Thread t = new Thread(...)未执行 t.start()

RUNNABLE

(可运行)

线程已启动(调用 start()),包含两种子状态:

① 正在 CPU 执行(Running)

② 等待 CPU 调度,线程此时还没正式开始执行(Ready)

1. t.start()从 NEW 状态 → RUNNABLE状态

2. 阻塞 / 等待状态恢复后回到 RUNNABLE状态

BLOCKED

(锁阻塞)

线程因争抢 synchronized 内置锁 失败而阻塞(仅针对 synchronized) 进入 synchronized 方法 / 代码块,但锁被其他线程持有

WAITING

(无限等待)

线程无限期等待,需其他线程主动唤醒(无超时),否则永久等待

调用无超时参数的方法:

1. Object.wait()(需先持锁,调用后释放锁)

2. Thread.join()(无超时)

3. LockSupport.park()

TIMED_WAITING(限期等待) 线程有限期等待超时自动唤醒,也可被提前唤醒

调用带超时参数的方法:

1. Thread.sleep(long)(最常用,不释放锁)

2. Object.wait(long)(释放锁)

3. Thread.join(long)

4. LockSupport.parkNanos()/parkUntil()

TERMINATED

(终止)

线程生命周期结束run() 正常执行完,或因未捕获异常终止

1. run() 方法执行完毕(正常终止)

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♪(・ω・)ノ

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐