Java中sleep和wait方法的功能差异
一、基础概念回顾
在深入探讨 sleep
和 wait
方法的差异之前,我们先来回顾一下这两个方法的基本概念。
(一)sleep
方法
sleep
方法是 Thread
类的静态方法,它使得当前正在执行的线程暂停执行指定的时间(以毫秒为单位),进入阻塞状态。在指定的时间过后,线程会重新进入可运行状态(Runnable),等待 CPU 调度再次执行。例如:
public class SleepExample {
public static void main(String[] args) {
System.out.println("线程开始执行");
try {
Thread.sleep(2000); // 线程暂停2秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程暂停结束,继续执行");
}
}
在上述代码中,当 Thread.sleep(2000)
被调用时,main
线程会暂停执行 2 秒钟,在这期间,线程不会执行后续的代码,2 秒过后,线程恢复执行,打印出“线程暂停结束,继续执行”。
(二)wait
方法
wait
方法是 Object
类的实例方法,这意味着任何对象都可以调用该方法。当一个线程调用某个对象的 wait
方法时,该线程会释放它持有的该对象的锁,并进入等待状态,直到其他线程调用该对象的 notify
或 notifyAll
方法来唤醒它。例如:
public class WaitExample {
public static void main(String[] args) {
Object lock = new Object();
Thread thread = new Thread(() -> {
synchronized (lock) {
System.out.println("线程获得锁并开始执行");
try {
lock.wait(); // 线程释放锁并进入等待状态
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程被唤醒,继续执行");
}
});
thread.start();
try {
Thread.sleep(2000); // 主线程暂停2秒
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock) {
System.out.println("主线程获得锁,唤醒等待线程");
lock.notify(); // 唤醒等待在lock对象上的线程
}
}
}
在这段代码中,子线程在获取 lock
对象的锁后,调用 lock.wait()
方法,此时子线程释放 lock
的锁并进入等待状态。主线程暂停 2 秒后,获取 lock
对象的锁,然后调用 lock.notify()
方法唤醒子线程,子线程被唤醒后重新获取 lock
的锁并继续执行。
二、所属类与调用方式差异
(一)所属类
sleep
方法:属于Thread
类,这是因为sleep
方法主要是针对线程本身的操作,它直接控制当前线程的暂停与恢复,与具体的对象实例没有直接关联。由于是静态方法,在调用时可以直接通过Thread.sleep()
的方式,不需要创建Thread
类的实例对象。例如在main
方法中,我们可以直接写Thread.sleep(1000)
让main
线程暂停 1 秒。wait
方法:属于Object
类,这意味着 Java 中的任何对象都具备wait
方法。这是因为wait
方法的设计理念是基于对象的监视器(Monitor)机制,与对象的锁紧密相关。每个对象都有自己的监视器,当线程调用对象的wait
方法时,实际上是在该对象的监视器上进行等待操作。例如我们创建一个String
对象str
,就可以在synchronized(str)
块中调用str.wait()
。
(二)调用方式
sleep
方法:由于是静态方法,调用方式固定为Thread.sleep(time)
,其中time
是指定线程暂停的时间,单位为毫秒。例如Thread.sleep(3000)
会让当前执行的线程暂停 3 秒钟。而且,sleep
方法不需要依赖于任何对象实例,它直接对当前线程进行操作。wait
方法:作为Object
类的实例方法,调用wait
方法前必须先获取对象的锁,即要在synchronized
块中调用。调用方式为对象实例.wait()
。例如:
Object obj = new Object();
synchronized (obj) {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
如果不在 synchronized
块中调用 wait
方法,会抛出 IllegalMonitorStateException
异常,因为只有在获取对象锁后,线程才有资格在该对象的监视器上等待。
三、锁的处理差异
(一)sleep
方法与锁
- 不释放锁:当线程调用
sleep
方法时,它不会释放已经获取的锁。这意味着在sleep
期间,其他线程如果尝试获取该线程持有的锁,将会被阻塞。例如:
public class SleepWithLockExample {
private static final Object lock = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock) {
System.out.println("线程1获得锁");
try {
Thread.sleep(5000); // 线程1暂停5秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1睡眠结束");
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock) {
System.out.println("线程2获得锁");
}
});
thread1.start();
try {
Thread.sleep(1000); // 主线程暂停1秒,确保线程1先获得锁
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.start();
}
}
在上述代码中,线程 1 首先获得 lock
对象的锁,然后调用 Thread.sleep(5000)
暂停 5 秒。在这 5 秒内,线程 2 尝试获取 lock
对象的锁,但由于线程 1 没有释放锁,线程 2 会一直处于阻塞状态,直到线程 1 睡眠结束并释放锁,线程 2 才能获得锁并继续执行。
2. 应用场景:这种特性使得 sleep
方法适用于一些需要让线程暂停一段时间,但又不希望其他线程在此期间干扰当前线程对共享资源操作的场景。比如在一个定时任务中,线程需要每隔一段时间执行一次某个操作,并且在执行操作过程中不希望其他线程修改相关的共享数据,就可以使用 sleep
方法。
(二)wait
方法与锁
- 释放锁:当线程调用
wait
方法时,它会立即释放当前持有的对象锁。这是wait
方法与sleep
方法最显著的区别之一。例如:
public class WaitWithLockExample {
private static final Object lock = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock) {
System.out.println("线程1获得锁");
try {
lock.wait(); // 线程1释放锁并进入等待状态
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1被唤醒并重新获得锁");
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock) {
System.out.println("线程2获得锁");
}
});
thread1.start();
try {
Thread.sleep(1000); // 主线程暂停1秒,确保线程1先进入等待状态
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.start();
}
}
在这个例子中,线程 1 获得 lock
对象的锁后,调用 lock.wait()
方法,此时线程 1 释放 lock
的锁并进入等待状态。线程 2 随后可以获取 lock
对象的锁并继续执行。当线程 2 执行完毕释放锁后,如果有其他线程调用 lock.notify()
唤醒线程 1,线程 1 会重新获取 lock
的锁并继续执行。
2. 应用场景:wait
方法释放锁的特性使得它非常适合用于线程间的协作场景。例如生产者 - 消费者模型中,当队列已满时,生产者线程调用 wait
方法释放锁并进入等待状态,消费者线程可以获取锁从队列中取出数据,当队列中有空间时,消费者线程调用 notify
方法唤醒生产者线程,生产者线程重新获取锁并继续生产数据。
四、唤醒机制差异
(一)sleep
方法的唤醒
- 时间到期唤醒:
sleep
方法是基于时间的唤醒机制。当线程调用sleep
方法并传入指定的时间参数后,在该时间到期后,线程会自动从阻塞状态转换为可运行状态(Runnable),等待 CPU 调度执行。例如:
public class SleepWakeUpExample {
public static void main(String[] args) {
System.out.println("线程开始执行");
try {
Thread.sleep(3000); // 线程暂停3秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程暂停结束,继续执行");
}
}
在上述代码中,线程调用 Thread.sleep(3000)
暂停 3 秒,3 秒后,线程会自动唤醒,继续执行后续代码。
2. 可被中断唤醒:除了时间到期唤醒外,sleep
方法还可以被中断唤醒。当其他线程调用正在 sleep
的线程的 interrupt
方法时,sleep
的线程会抛出 InterruptedException
异常并提前唤醒。例如:
public class SleepInterruptExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("线程开始执行");
try {
Thread.sleep(10000); // 线程暂停10秒
} catch (InterruptedException e) {
System.out.println("线程被中断唤醒");
}
System.out.println("线程结束执行");
});
thread.start();
try {
Thread.sleep(3000); // 主线程暂停3秒
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt(); // 中断线程
}
}
在这个例子中,子线程调用 Thread.sleep(10000)
暂停 10 秒,但主线程在 3 秒后调用 thread.interrupt()
中断子线程,子线程会抛出 InterruptedException
异常并提前唤醒,打印出“线程被中断唤醒”,然后继续执行后续代码。
(二)wait
方法的唤醒
notify
方法唤醒:wait
方法的线程需要通过其他线程调用同一个对象的notify
方法来唤醒。notify
方法会随机唤醒一个等待在该对象上的线程。例如:
public class WaitNotifyExample {
private static final Object lock = new Object();
public static void main(String[] args) {
Thread waitingThread = new Thread(() -> {
synchronized (lock) {
System.out.println("等待线程获得锁并开始等待");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("等待线程被唤醒");
}
});
Thread notifyingThread = new Thread(() -> {
synchronized (lock) {
System.out.println("通知线程获得锁");
lock.notify();
System.out.println("通知线程发出通知");
}
});
waitingThread.start();
try {
Thread.sleep(1000); // 主线程暂停1秒,确保等待线程先进入等待状态
} catch (InterruptedException e) {
e.printStackTrace();
}
notifyingThread.start();
}
}
在上述代码中,waitingThread
线程调用 lock.wait()
进入等待状态,notifyingThread
线程在获取锁后调用 lock.notify()
方法,随机唤醒 waitingThread
线程。
2. notifyAll
方法唤醒:与 notify
方法不同,notifyAll
方法会唤醒所有等待在该对象上的线程。这些被唤醒的线程会竞争对象的锁,只有获得锁的线程才能继续执行。例如:
public class WaitNotifyAllExample {
private static final Object lock = new Object();
public static void main(String[] args) {
Thread waitingThread1 = new Thread(() -> {
synchronized (lock) {
System.out.println("等待线程1获得锁并开始等待");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("等待线程1被唤醒");
}
});
Thread waitingThread2 = new Thread(() -> {
synchronized (lock) {
System.out.println("等待线程2获得锁并开始等待");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("等待线程2被唤醒");
}
});
Thread notifyingThread = new Thread(() -> {
synchronized (lock) {
System.out.println("通知线程获得锁");
lock.notifyAll();
System.out.println("通知线程发出通知");
}
});
waitingThread1.start();
waitingThread2.start();
try {
Thread.sleep(1000); // 主线程暂停1秒,确保等待线程先进入等待状态
} catch (InterruptedException e) {
e.printStackTrace();
}
notifyingThread.start();
}
}
在这个例子中,waitingThread1
和 waitingThread2
两个线程都调用 lock.wait()
进入等待状态,notifyingThread
线程获取锁后调用 lock.notifyAll()
方法,唤醒这两个等待线程,它们会竞争 lock
对象的锁,获得锁的线程会继续执行。
五、使用场景差异
(一)sleep
方法的使用场景
- 定时任务:在很多应用中,我们需要定时执行某些任务,例如定时备份数据、定时清理缓存等。
sleep
方法可以方便地实现这种定时功能。例如:
public class ScheduledTaskExample {
public static void main(String[] args) {
while (true) {
System.out.println("执行定时任务");
try {
Thread.sleep(5000); // 每隔5秒执行一次任务
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
在上述代码中,通过 while (true)
循环和 Thread.sleep(5000)
实现了每隔 5 秒执行一次“执行定时任务”的操作。
2. 模拟延迟:在开发过程中,有时候需要模拟网络延迟、系统响应延迟等情况来测试程序的稳定性和性能。sleep
方法可以用来模拟这种延迟。例如:
public class SimulateDelayExample {
public static void main(String[] args) {
System.out.println("开始模拟延迟");
try {
Thread.sleep(3000); // 模拟3秒延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("延迟结束,继续执行");
}
}
在这个例子中,通过 Thread.sleep(3000)
模拟了 3 秒的延迟,这在测试一些需要处理延迟情况的代码时非常有用。
(二)wait
方法的使用场景
- 生产者 - 消费者模型:这是
wait
方法最典型的应用场景。在生产者 - 消费者模型中,生产者线程生产数据并放入共享队列,消费者线程从共享队列中取出数据。当队列已满时,生产者线程需要等待,直到消费者线程从队列中取出数据,队列有空间时再继续生产;当队列已空时,消费者线程需要等待,直到生产者线程向队列中放入数据。例如:
import java.util.LinkedList;
import java.util.Queue;
public class ProducerConsumerExample {
private static final int MAX_SIZE = 5;
private static final Queue<Integer> queue = new LinkedList<>();
public static void main(String[] args) {
Thread producerThread = new Thread(() -> {
int value = 0;
while (true) {
synchronized (queue) {
while (queue.size() == MAX_SIZE) {
try {
queue.wait(); // 队列已满,生产者等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.add(value++);
System.out.println("生产者生产: " + (value - 1));
queue.notify(); // 唤醒消费者线程
}
}
});
Thread consumerThread = new Thread(() -> {
while (true) {
synchronized (queue) {
while (queue.isEmpty()) {
try {
queue.wait(); // 队列已空,消费者等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int value = queue.poll();
System.out.println("消费者消费: " + value);
queue.notify(); // 唤醒生产者线程
}
}
});
producerThread.start();
consumerThread.start();
}
}
在上述代码中,生产者线程和消费者线程通过 wait
和 notify
方法在共享队列 queue
上进行协作,实现了数据的生产和消费。
2. 线程间协作:除了生产者 - 消费者模型,wait
方法还适用于其他线程间协作的场景,例如多个线程共同完成一个复杂任务,某个线程需要等待其他线程完成部分工作后才能继续执行。例如:
public class ThreadCooperationExample {
private static final Object lock = new Object();
private static boolean taskCompleted = false;
public static void main(String[] args) {
Thread workerThread = new Thread(() -> {
synchronized (lock) {
System.out.println("工作线程开始工作");
// 模拟工作过程
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
taskCompleted = true;
System.out.println("工作线程完成工作");
lock.notify(); // 唤醒等待线程
}
});
Thread waitingThread = new Thread(() -> {
synchronized (lock) {
while (!taskCompleted) {
try {
lock.wait(); // 等待工作线程完成工作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("等待线程继续执行");
}
});
waitingThread.start();
workerThread.start();
}
}
在这个例子中,waitingThread
线程等待 workerThread
线程完成工作,通过 wait
和 notify
方法实现了线程间的协作。
六、异常处理差异
(一)sleep
方法的异常处理
InterruptedException
异常:sleep
方法会抛出InterruptedException
异常。这是因为在sleep
期间,线程可能会被其他线程调用interrupt
方法中断。当线程被中断时,sleep
方法会提前返回,并抛出InterruptedException
异常。例如:
public class SleepExceptionExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("线程开始执行");
try {
Thread.sleep(10000); // 线程暂停10秒
} catch (InterruptedException e) {
System.out.println("线程被中断,捕获异常");
}
System.out.println("线程结束执行");
});
thread.start();
try {
Thread.sleep(3000); // 主线程暂停3秒
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt(); // 中断线程
}
}
在上述代码中,子线程调用 Thread.sleep(10000)
暂停 10 秒,主线程在 3 秒后调用 thread.interrupt()
中断子线程,子线程捕获 InterruptedException
异常并打印“线程被中断,捕获异常”,然后继续执行后续代码。
2. 异常处理策略:在捕获到 InterruptedException
异常后,通常有几种处理策略。一种是直接打印异常堆栈信息,如 e.printStackTrace()
,这种方式适用于调试阶段,方便开发人员定位问题。另一种是根据业务需求进行适当的处理,例如记录日志、清理资源或者重新尝试执行任务等。例如:
public class SleepExceptionHandlingExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("线程开始执行");
try {
Thread.sleep(10000); // 线程暂停10秒
} catch (InterruptedException e) {
System.out.println("线程被中断,记录日志");
// 记录日志
// 清理资源
// 重新尝试执行任务
}
System.out.println("线程结束执行");
});
thread.start();
try {
Thread.sleep(3000); // 主线程暂停3秒
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt(); // 中断线程
}
}
在这个例子中,当捕获到 InterruptedException
异常时,打印“线程被中断,记录日志”,并可以根据实际需求在注释部分添加记录日志、清理资源或重新尝试执行任务的代码。
(二)wait
方法的异常处理
InterruptedException
异常:wait
方法同样会抛出InterruptedException
异常。当线程在等待过程中被其他线程调用interrupt
方法中断时,wait
方法会提前返回并抛出InterruptedException
异常。例如:
public class WaitExceptionExample {
private static final Object lock = new Object();
public static void main(String[] args) {
Thread waitingThread = new Thread(() -> {
synchronized (lock) {
System.out.println("等待线程获得锁并开始等待");
try {
lock.wait();
} catch (InterruptedException e) {
System.out.println("等待线程被中断,捕获异常");
}
System.out.println("等待线程结束执行");
}
});
waitingThread.start();
try {
Thread.sleep(3000); // 主线程暂停3秒
} catch (InterruptedException e) {
e.printStackTrace();
}
waitingThread.interrupt(); // 中断等待线程
}
}
在上述代码中,waitingThread
线程调用 lock.wait()
进入等待状态,主线程在 3 秒后调用 waitingThread.interrupt()
中断该线程,waitingThread
捕获 InterruptedException
异常并打印“等待线程被中断,捕获异常”,然后继续执行后续代码。
2. 与 sleep
异常处理的异同:与 sleep
方法捕获 InterruptedException
异常后的处理类似,wait
方法捕获该异常后也可以根据业务需求进行处理。不同之处在于,wait
方法是基于对象的监视器机制,在捕获异常后可能还需要考虑对象锁的状态以及与其他线程的协作情况。例如,在生产者 - 消费者模型中,如果消费者线程在 wait
时被中断,可能需要在处理异常后重新检查队列状态,以确保后续操作的正确性。例如:
import java.util.LinkedList;
import java.util.Queue;
public class ProducerConsumerExceptionExample {
private static final int MAX_SIZE = 5;
private static final Queue<Integer> queue = new LinkedList<>();
public static void main(String[] args) {
Thread producerThread = new Thread(() -> {
int value = 0;
while (true) {
synchronized (queue) {
while (queue.size() == MAX_SIZE) {
try {
queue.wait(); // 队列已满,生产者等待
} catch (InterruptedException e) {
System.out.println("生产者线程被中断,重新检查队列状态");
// 重新检查队列状态
// 处理异常相关操作
}
}
queue.add(value++);
System.out.println("生产者生产: " + (value - 1));
queue.notify(); // 唤醒消费者线程
}
}
});
Thread consumerThread = new Thread(() -> {
while (true) {
synchronized (queue) {
while (queue.isEmpty()) {
try {
queue.wait(); // 队列已空,消费者等待
} catch (InterruptedException e) {
System.out.println("消费者线程被中断,重新检查队列状态");
// 重新检查队列状态
// 处理异常相关操作
}
}
int value = queue.poll();
System.out.println("消费者消费: " + value);
queue.notify(); // 唤醒生产者线程
}
}
});
producerThread.start();
consumerThread.start();
}
}
在上述代码中,生产者和消费者线程在捕获 InterruptedException
异常后,打印“重新检查队列状态”,并可以根据实际需求在注释部分添加重新检查队列状态和处理异常相关操作的代码。
七、总结与最佳实践建议
(一)总结
- 所属类与调用方式:
sleep
方法属于Thread
类,是静态方法,通过Thread.sleep()
调用;wait
方法属于Object
类,是实例方法,必须在synchronized
块中通过对象实例调用。 - 锁的处理:
sleep
方法不释放锁,而wait
方法会释放当前持有的对象锁。 - 唤醒机制:
sleep
方法基于时间到期或被中断唤醒,wait
方法需要其他线程调用notify
或notifyAll
方法唤醒。 - 使用场景:
sleep
方法适用于定时任务和模拟延迟等场景,wait
方法适用于线程间协作,如生产者 - 消费者模型。 - 异常处理:两者都会抛出
InterruptedException
异常,但wait
方法在处理异常时还需考虑对象锁状态和线程协作情况。
(二)最佳实践建议
- 根据场景选择方法:在编写多线程代码时,首先要明确需求场景。如果是简单的定时操作或模拟延迟,优先选择
sleep
方法;如果涉及线程间复杂的协作,如数据共享和同步,wait
方法更合适。 - 注意锁的管理:使用
sleep
方法时要注意线程持有锁可能导致其他线程阻塞,避免死锁。使用wait
方法时,要确保在正确的synchronized
块中调用,并合理处理锁的释放和获取。 - 优雅处理异常:无论是
sleep
还是wait
方法抛出的InterruptedException
异常,都要根据业务需求进行优雅处理,避免简单忽略异常导致程序出现潜在问题。 - 代码结构清晰:在使用这两个方法时,要保持代码结构清晰,便于理解和维护。例如,在生产者 - 消费者模型中,将生产和消费逻辑分别封装在独立的方法或类中,使代码层次更分明。
通过深入理解 sleep
和 wait
方法的差异,并遵循最佳实践建议,开发人员可以编写出更健壮、高效的多线程 Java 程序。