在 Java 中,线程的 休眠(sleep)等待(wait)加入(join)让步(yield) 是四种不同的线程协作或控制方式,它们的作用、机制和使用场景各不相同。

在线程的生命周期中,不同状态之间切换时,可以通过调用sleep()、wait()、join()、yield()等方法进行线程状态控制。

img

以下是它们的详细区别:

方法 是否释放锁 是否阻塞 是否需要同步上下文 是否可中断 用法 典型用途
wait() Object.wait 线程间协作(生产者-消费者)
sleep() Thread.sleep 定时暂停
join() Thread.join 等待线程结束。
yield() Thread.yield 礼让 CPU(提示)

线程的5种状态:

img
  • 新建状态(New) : 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread(),且threadStatus = 0。

  • 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start(),就绪状态的线程,随时可能被CPU调度执行。

  • 运行状态(Running) : 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。

  • 阻塞状态(Blocked) : 线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。在等待进入一个临界区,阻塞的情况分三种:

    • 等待阻塞(等待唤醒)(in Object.wait()) – 通过调用线程的**wait()**方法,让线程等待某工作的完成。位于对象等待池中的阻塞状态(Blocked in object’s wait pool),涉及线程通信。如果大量线程在该状态,获得了监视器之后,又调用了wait() 方法
    • 同步阻塞(等待获取监视器)(waiting for monitor entry) – 线程获取同步锁失败 (因为锁被其它线程所占用),它会进入同步阻塞状态。位于对象锁池中的阻塞状态(Blocked in object’s lock pool),涉及线程同步。如果大量线程在该状态,可能是争夺一个全局锁而被阻塞(某线程在临界区时间太长,以至于新线程迟迟无法进入临界区)
    • 其他阻塞(等待资源)(waiting on condition) – 通过调用线程的**sleep()或join()**或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。如果大量线程在该状态,可能获取第三方资源网络阻塞,迟迟得不到响应,导致大量线程进入等待状态。或者IO读写较慢。
  • 消亡状态(Dead) : 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

休眠(sleep)

作用:让当前线程暂停执行指定的时间,但不会释放锁(不释放已持有的对象锁)。

  • 通过设置方法中的时间参数,使调用它的线程休眠指定时间,线程从Running(运行)状态转为Blocked(阻塞)状态。时间结束后自动恢复运行(进入就绪状态 Runnable )。

  • 这个过程中会释放CPU资源,给其他线程运行机会时不考虑线程的优先级,但如果有同步锁则不会释放锁,其他线程无法获得同步锁。

stateDiagram-v2
    [*] --> Runnable
    Runnable --> Sleeping: 调用 Thread.sleep(millis)
    Sleeping --> Runnable: 休眠时间结束 或 被中断 (InterruptedException)
    Sleeping --> [*]: 线程终止(可选路径)

休眠时间未到时,可通过调用interrupt()方法来唤醒休眠线程。

1
2
3
4
5
try {
Thread.sleep(200); //暂停200毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}

sleep是线程级别的休眠,不涉及到对象类,只是让当前线程暂停,进入休眠状态,并不释放同步锁资源,也不需要去获得对象锁。

  • 不释放已持有的对象锁(即使在 synchronized 块中)。

  • 会抛出 InterruptedException,必须处理。

  • 时间结束后自动恢复运行(进入就绪状态)。

等待(wait)

作用:让当前线程在某个对象上等待,会释放该对象的锁,直到被其他线程唤醒(notify/notifyAll)。

它是Object类的成员本地方法,会让持有对象锁的线程释放锁,并进入线程等待池中等待被唤醒,即在池中竞争同步锁,同时释放CPU资源。需要配合其他方法使用:notify(随机或顺序唤醒),notifyAll全部唤醒,线程结束自动唤醒

  • notify 的唤醒顺序取决于JVM的实现,可能是随机,可能是顺序唤醒
  • 在hotspot 虚拟机中,是顺序唤醒,每次取出第一个等待的元素
stateDiagram-v2
    [*] --> Runnable
    Runnable --> Blocked: 尝试进入 synchronized 块(获取锁)
    Blocked --> Runnable: 成功获取对象监视器锁
    Runnable --> Waiting: 调用 obj.wait()
    Waiting --> Runnable: 其他线程调用 obj.notify() 或 obj.notifyAll()
    Waiting --> Runnable: 被中断(抛出 InterruptedException)
    Waiting --> [*]: 线程结束(可选)

它的调用必须在同步方法或同步代码块中执行,也需要捕获 InterruptedException 异常。

1
2
3
4
5
6
7
8
9
10
//同步代码块
synchronized (obj) {
System.out.println("obj to wait on RunnableImpl1");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("obj continue to run on RunnableImpl1");
}

每个对象都拥有各自的对象锁,wait的作用是释放当前线程占有的对象锁,自然是要操作对应的Object而不是Thread

  • 必须在同步上下文中调用(否则抛出 IllegalMonitorStateException)。

  • 会释放对象锁,允许其他线程进入同步块。

  • 可设置超时时间(wait(long timeout)),超时后自动唤醒。

  • 通常与 notify()/notifyAll() 配合使用,实现线程间通信。

加入(join)

作用:让当前线程等待另一个线程执行完毕后再继续执行。

调用join()的线程拥有优先使用CPU时间片的权利,其他线程需要等待join()调用线程执行结束后才能继续执行

stateDiagram-v2
    [*] --> MainRunning
    [*] --> ChildRunning

    state "主线程" as MainRunning
    state "子线程" as ChildRunning
    state "主线程阻塞等待" as MainWaiting
    state "子线程终止" as ChildTerminated

    MainRunning --> MainWaiting: 调用 childThread.join()
    ChildRunning --> ChildTerminated: 子线程执行完毕
    MainWaiting --> MainRunning: 子线程终止(join 返回)
    
    ChildTerminated --> [*]
    MainRunning --> [*]

    note right of MainWaiting
      主线程进入 WAITING 或 TIMED_WAITING 状态,
      等待子线程结束。
    end note

    note left of ChildRunning
      子线程独立运行,
      不受 join 影响。
    end note
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//创建TestRunnable类
TestRunnable mr = new TestRunnable();
//创建Thread类的有参构造,并设置线程名
Thread t1 = new Thread(mr, "t1");
Thread t2 = new Thread(mr, "t2");
Thread t3 = new Thread(mr, "t3");
//启动线程
t1.start();
try {
t1.join(); //等待t1执行完才会轮到t2,t3抢
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
t3.start();

底层主要通过wait()实现,参数代表等待当前线程最多执行多少毫秒,如果 为 0,则会一直执行,直至完成,才会轮到其他线程继续;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;

if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}

if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
  • 调用 t.join() 的线程会阻塞,直到 t 终止。

  • 内部使用了 wait() 机制(但对用户透明)。

  • 也会抛出 InterruptedException

让步(yield)

作用提示调度器Thread.yield() 让当前线程主动让出 CPU 资源,给其他相同优先级或更高优先级的线程运行的机会

提出申请释放CPU资源,在多线程编程中可以用来影响线程执行顺序和资源分配,至于能否成功释放由JVM决定

  1. 避免线程长时间占用 CPU

    • 在某些情况下,一个线程可能会长时间占用 CPU 资源,导致其他线程无法得到执行的机会。这时,可以在适当的时候调用yield方法,让当前线程暂停一下,给其他线程一个执行的机会。例如,在一个复杂的计算任务中,如果一个线程一直在进行计算而不释放 CPU 资源,可能会导致其他线程无法及时响应用户的操作。通过在计算过程中定期调用yield方法,可以让其他线程有机会执行,提高系统的响应性。
  2. 平衡线程的执行时间

    • 在多线程环境中,不同的线程可能具有不同的执行时间和优先级。如果某些线程的执行时间过长,可能会导致其他线程等待时间过长,影响系统的整体性能。通过在长执行时间的线程中适当调用yield方法,可以让其他线程有机会执行,从而平衡各个线程的执行时间,提高系统的整体效率。
  3. 提高线程的公平性

    • 在某些情况下,线程的调度可能会出现不公平的情况,导致某些线程长时间无法得到执行的机会。通过在适当的时候调用yield方法,可以让当前线程主动让出 CPU 资源,给其他线程一个执行的机会,从而提高线程的公平性。

原理

给相同优先级或更高优先级的线程运行的机会执行权(也可能是自己本身),自己会处于就绪状态

但是线程优先级高的也不一定的获得执行权, 优先级高仅仅只是执行概率大了一点。并且所谓的优先级执行,是在大量执行次数中才能体现出来的。

stateDiagram-v2
    [*] --> Runnable

    Runnable --> Yielding: 调用 Thread.yield()
    Yielding --> Runnable: 线程调度器重新调度(可能立即回到运行)

    note right of Yielding
      yield() 建议当前线程让出 CPU,
      但不保证其他线程一定获得执行权。
      线程仍处于 RUNNABLE 状态(Java 中无独立“Yielding”状态)。
    end note

sleep()方法调用后线程处于阻塞TIME_WAITING状态,所以yield()方法调用后线程只是暂时的将调度权让给别人,但立刻可以回到竞争线程锁的状态;

Yield 是一种启发式尝试,旨在改善线程之间的相对进展,否则会过度使用 CPU。 它的使用应与详细的分析和基准测试相结合,以确保它确实具有预期的效果。很少适合使用这种方法。 它对于调试或测试目的可能很有用,它可能有助于重现由于竞争条件引起的错误。 在设计并发控制结构(例如java.util.concurrent.locks包中的结构)时,它也可能很有用。

  1. 不确定性
    • yield方法只是一个提示性的方法,它不能保证当前线程一定会让出 CPU 资源,也不能保证其他线程一定会被选中执行。因此,在使用yield方法时,不能依赖它来实现特定的线程执行顺序。
  2. 性能影响
    • 频繁地调用yield方法可能会对性能产生一定的影响。因为每次调用yield方法都会导致当前线程进入就绪状态,然后由调度器重新选择下一个要执行的线程,这个过程可能会消耗一定的时间和系统资源。
  3. 优先级问题
    • yield方法并不能改变线程的优先级。如果一个线程的优先级较高,即使其他线程调用了yield方法,调度器仍然可能会优先选择高优先级的线程执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Test {

public static void main(String[] args) {
Thread thread1 = new Thread(Test::printNumbers, "小明");
Thread thread2 = new Thread(Test::printNumbers, "小华");
thread1.start();
thread2.start();
}
private static void printNumbers() {
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);

// 当 i 是偶数时,当前线程暂停执行
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + " 让出控制权...");
Thread.yield();
}
}
}
}
// 小明: 1
// 小华: 1
// 小华: 2
// 小明: 2
// 小明 让出控制权...
// 小华 让出控制权...
// 小明: 3
// 小明: 4
// 小明 让出控制权...
// 小明: 5
// 小华: 3
// 小华: 4
// 小华 让出控制权...
// 小华: 5
  • 不保证一定让出 CPU,只是建议(JVM 可能忽略)。

  • 不会阻塞线程(线程仍处于 RUNNABLE 状态)。

  • 不释放锁。

  • 通常用于调试或测试,生产代码中很少使用。