Java多线程系列(二)——多线程的调度
前言
在上一篇文章中,我们介绍了多线程的基本概念和生命周期等内容,本篇文章将着重对多线程的调度,即多线程状态的切换方法进行讲解,主要会对
sleep()
、wait()
、notify()
等方法的使用进行介绍,让读者了解各个方法的使用场景,从而在实际应用中可以更好的选择合适的方法。
一、线程的睡眠、等待和让步
(一)线程的sleep()方法
在Java中,线程的sleep方法是经常被使用到的方法,它可以使当前的线程在接下来的指定时间内进入睡眠状态,也就是TIMED_WAITING
状态。我们可以通过下面的案例来看一下sleep的使用方式:
public class ThreadSchedualTest {
public static void main(String[] args) {
sleepTest();
}
private static void sleepTest() {
SimpleDateFormat dataFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(dataFormat.format(new Date()));
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(dataFormat.format(new Date())+" , two seconds passed...");
}
}

(二)线程的wait()方法
与sleep方法类似,wait方法也可以让进程进入WAITING
或者TIMED_WAITING
的状态中(至于是哪一种状态取决于wait方法有没有传入具体的时间参数进去)。而当线程进入等待状态时,就需要其他进程使用nofity()
方法或者notifyAll()
的方法才可以唤醒进程,需要注意的是notify()
方法只能从等待队列中随机挑选一个等待中的进程进行唤醒,而不一定能唤醒我们真正想要唤醒的队列。
我们来看一下下面使用wait()方法的案例:
public class ThreadSchedualTest {
public static void main(String[] args) {
Thread thread = new Thread(new NotifyThread());
thread.start();
synchronized (thread) {
try {
System.out.println("let the main thread have a rest");
thread.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("main thread has been woken up");
}
class NotifyThread implements Runnable {
@Override
public void run() {
synchronized (this) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.notify();
System.out.println("the notifyThread notify the main thread...");
}
}
}
我们来对上面的方法进行解释,首先main主线程启动了新的notifyThread
线程之后,进入了等待状态,需要注意的是,虽然thread对象是NotifyThread类产生的,但是实际上真正进入等待状态的是main
线程。同时,notifyThread
线程中先执行了sleep()
方法睡眠了2秒后,执行了notify()
方法,因为此时只有main方法处于waiting状态,所以唤醒的自然就是main主线程。

需要注意的是,我们在上述的代码中使用了synchronized
关键字,并且wait()
、notify()
方法都需要放在synchronized
修饰的代码块中,这样做的原因是什么呢?
先说说synchronized
关键字在此处的作用,限制代码块中的方法只有获得对象实例锁的一条线程可以调用。
而wait()
方法需要放在synchronized
代码块中原因是:执行wait()
方法时,会要求当前线程放开所持有的对象锁,放开对象锁的前提是拥有这个对象锁,否则就会报错(因为没有对象锁可以让线程放开)。这种情况下,如果wait()
方法是写在synchronized
代码块中,就能够保证当前的线程是一定持有锁的。也可以把synchronized
关键字当做是一个拥有锁的标识flag。
那么为什么notify()
方法也需要用到synchronized
关键字呢?
答案其实和wait()
方法有关系,在没有设置等待时长的前提下,一条线程进入等待状态之后就势必需要有其他一条线程执行notify()
或者notifyAll()
方法来唤醒它。如果没有同步锁的存在,当出现notify()
方法先执行,wait()
方法后执行的情况时,那么进入等待状态的线程将无法被唤醒!!
更加具体的细节可以参考这篇文章:为什么wait()和notify()需要搭配synchonized关键字使用?
(三)线程的yield方法
yield()
方法和wait()
和sleep()
方法相似,都可以让线程退出运行状态一段时间,而后再次运行。但yield()
方法和后两者不同的地方在于线程的状态,yield()
方法是将线程从运行状态转为就绪状态,本质上还是Runnable
状态,也可以理解为yield()
方法只是让线程让出了CPU资源,但实际上它还是可运行状态的。
我们可以通过下面的案例来了解一下yield()
方法的使用。
public class ThreadSchedualTest {
public static void main(String[] args) {
yieldTest();
}
private static void yieldTest() {
Runnable runnable = () -> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
if (i == 5) {
System.out.println("thread is yield...");
Thread.yield();
}
}
};
Runnable myRunImpl = runnable;
Thread thread1 = new Thread(myRunImpl, "THREAD-01");
Thread thread2 = new Thread(myRunImpl, "THREAD-02");
thread1.start();
thread2.start();
}
}

演示的结果很有意思,一开始线程1执行到数字5时,线程调用yield()
方法让出资源,此时线程2开始执行,而执行到数字5时,线程2也调用了yield()
方法让出了资源,但接下来执行的线程还是线程2。这是因为yield()
方法只是让线程重新处于就绪状态,也就是说它和线程1还是处于争夺CPU资源的状态,如果CPU接下来还是决定把资源分配给线程2的话,那就还是线程2先执行。
二、多线程的唤醒
多线程的唤醒其实就两个方法:notify()
和notifyAll()
,前者可以将一条状态为WAITING
的线程转为RUNNABLE
状态,但这种方法唤醒的线程是随机的。下面我们将使用一个案例来演示一下这种特点:
public class ThreadSchedualTest {
public static void main(String[] args) {
notifyTest();
}
private static void notifyTest() {
Object objectKey = new Object();
Thread thread1 = new Thread(new WaitingRunnable(objectKey),"Thread-01");
Thread thread2 = new Thread(new WaitingRunnable(objectKey),"Thread-02");
Thread thread3 = new Thread(new WaitingRunnable(objectKey),"Thread-03");
Thread thread4 = new Thread(new WaitingRunnable(objectKey),"Thread-04");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
try {
// 让主线程先睡眠一下,防止出现部分线程没有执行wait方法的情况发生
System.out.println("the main thread will sleep 2 seconds");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objectKey){
System.out.println("the main thread has waken up...");
objectKey.notify();
objectKey.notify();
objectKey.notify();
objectKey.notify();
}
}
}
class WaitingRunnable implements Runnable {
// 监控锁
private Object monitorLock;
public WaitingRunnable(Object monitorLock){
this.monitorLock = monitorLock;
}
@Override
public void run() {
synchronized (monitorLock){
try {
System.out.println(Thread.currentThread().getName() + " now , let the thread have a rest...");
monitorLock.wait();
System.out.println(Thread.currentThread().getName() + " now , the thread is wake up.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

从上面的结果我们可以看出,所有状态为
WAITING
的线程最终都被成功唤醒了,第二是唤醒的线程顺序和一开始进入WAITING
状态的线程顺序并不相同,这也就印证了notify()
方法的随机性。
三、多线程的插队
线程的插队分为主动插队和被动插队,其中主动插队使用join()
方法进行,而被动插队一般是通过调大线程的优先级来实现的,我们在上一篇文章中就讲过,调大优先级并不能保证优先级高的队列一定会先执行。
实际上,多线程的主动插队类似是线程同步的效果,也就是说它把并行的多线程变为了串行的运行效果。具体我们可以来看一下下面这个案例:
public class ThreadSchedualTest {
public static void main(String[] args) {
joinTest();
}
private static void joinTest() {
Thread thread = new Thread(new JoinOtherRunnable());
for (int i = 0; i < 20; i++) {
System.out.println("the main thread is counting ,the num is "+i);
if(i == 10){
try {
thread.start();
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class JoinOtherRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("the other thread is counting, num is "+i);
}
}
}

从上面的结果我们可以看到,当主线程调用其他线程的
join()
方法时,主线程会等待该线程执行完毕后才能够继续执行。
有关线程调度的API我们就讲解得差不多了,最后我们来对sleep()和wait()方法做一个对比:
方法所属 | 是否释放锁 | |
---|---|---|
wait | wait方法是Object方法中继承的 | 释放 |
sleep | sleep方法是Thread类才有的 | 不释放 |
至此,本篇文章的内容到此结束,想要了解更多关于多线程的知识,可以关注本系列的其他文章。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 上周热点回顾(3.3-3.9)