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类才有的 不释放

至此,本篇文章的内容到此结束,想要了解更多关于多线程的知识,可以关注本系列的其他文章。

posted @   moutory  阅读(31)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示