Thread类中的常用线程调度方法sleep、yield、join

发布时间 2023-06-23 09:46:23作者: 哩个啷个波

sleep

sleep方法是在Thread类中的一个静态方法,当一个线程调用了sleep方法,被调用的那个线程就会暂时的让出指定时间的CPU执行权,在这段时间也不会参与CPU的调度,当时间到了之后,就会重新回到就绪状态,等待CPU的再次调度,注意是就绪状态,而不是重新拿回CPU的执行权。并且,在休眠期间,只是会让出CPU的执行权,但是之前获得的锁资源,还是继续持有,等CPU调度到该线程重新获取到执行权的时候,就会继续运行sleep之后的代码。接下来用一个例子来说明Sleep期间不会放弃锁资源

public class SleepDemo {
    public static void main(String[] args) throws InterruptedException {
        Lock lock = new ReentrantLock();
        Thread one = new Thread(() -> {
            lock.lock();
            System.out.println("线程A准备被Sleep"); //1
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程A被唤醒"); //2
            lock.unlock();
        });
        Thread two = new Thread(() -> {
            lock.lock();
            System.out.println("线程B准备被Sleep"); //3
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程B被唤醒"); //4
            lock.unlock();
        });
        one.start();
        two.start();
    }
}

在上面的代码中,new了两个线程,但是只有一个锁资源lock,如果sleep期间会释放锁资源的话,那么输出结果应当是交替输出的,因为在执行代码1的时候,会被剥夺执行权,那么线程2将会拿到锁,但是事实真的是这样吗,看输出结果

线程A准备被Sleep
线程A被唤醒
线程B准备被Sleep
线程B被唤醒

可以看到,线程A被sleep的时候,线程2执行,想要获取锁I资源lock,但是发现lock被占有了,只好阻塞,然后线程1的休眠时间到了,重新获取执行权,继续执行sleep之后的代码,然后释放锁,然后线程2拿到锁,执行后续代码。通过上面的代码就可以看出,sleep期间是不会释放锁资源的,只是暂时的让出了cpu的执行权。

yield

yield和sleep在功能上有点像。yield方法是Thread中的一个静态方法,当一个线程调用yield方法的时候,则会直接让出自己cpu执行权,并且直接到就绪状态,也就是说,调用yield不会让线程阻塞。举个通俗点的例子,我们都知道,操作系统调度线程的时候,会有一个时间片机制,当时间片用完之后,就会回到就绪状态等待cpu的下一次调度,如果A线程在他的时间片还没用完之前,调用了yield方法,就代表着,放弃还未用完的时间片,告诉线程调度器,还有多的时间片我不想用了,你直接进行下一轮调度吧,然后A线程也回到了就绪状态。请注意,调用了yield的线程,是回到了就绪状态,而不是阻塞,这意味着,即便他刚调用玩yield方法,放弃了自己的执行权,但是在接下来的线程调度中,仍然还有可能获得cpu的执行权。

public class YieldDemo1 {
    public static void main(String[] args) {
        Thread one = new Thread(() -> {

            for (int i = 0; i < 20; i++) {
                if (i%5 == 0){
                    System.out.println(Thread.currentThread().getName()+"调用yield放弃了cpu执行权");
                    Thread.yield();
                }
            }
            System.out.println(Thread.currentThread().getName()+"结束");
        },"线程1");
        Thread two = new Thread(() -> {
            for (int i = 0; i < 20; i++) {
                if (i%5 == 0){
                    System.out.println(Thread.currentThread().getName()+"调用yield放弃了cpu执行权");
                    Thread.yield();
                }
            }
            System.out.println(Thread.currentThread().getName()+"结束");
        },"线程2");

        one.start();
        two.start();
    }
}

输出结果

线程1调用yield放弃了cpu执行权
线程2调用yield放弃了cpu执行权
线程1调用yield放弃了cpu执行权
线程1调用yield放弃了cpu执行权
线程1调用yield放弃了cpu执行权
线程2调用yield放弃了cpu执行权
线程1结束
线程2调用yield放弃了cpu执行权
线程2调用yield放弃了cpu执行权
线程2结束

可以看看第三、四、五行输出,在线程1放弃了cpu执行权,继续调度的时候,下一次调度还是调度到了线程1.这也验证了我们上面的说法。

  • 总结:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态。而yield和sleep的区别在于,调用了sleep方法之后,当前调用线程将会被阻塞指定时间,在这期间不会参与调度,而yield只是放弃了自己的时间片执行权,并不会被阻塞,直接以就绪状态去参与下一次的线程调度,下次调度还是有可能会被调度到。

join

在我们平常的编程里,经常会碰到这样的一个场景,例如,我们有A,B,C三种线程,其中A类线程是用于读数据的,然后B类线程是处理数据的,把A类线程读的数据进行汇总,C线程是写入数据的,需要用到A线程读取的数据,并且要等数据全部读取完毕,也就是A线程结束了,B线程才开始处理数据,等B类线程全部处理完毕之后,C类线程才能写入数据。这个场景很常见,多线程加载资源,加载完毕再统一汇总处理。

以上这种场景,如果我们需要自己手动控制判断所有的线程结束了才执行下面的任务,其实是比较麻烦的,因此Thread提供了一个join方法,可以阻塞当前正在运行的线程,让调用join的线程运行完毕之后,当前线程才能继续运行。看代码就明白了

public class join {
    public static void main(String[] args) throws InterruptedException {
        Thread one = new Thread(() ->{
            try {
                System.out.println("线程1");
                Thread.sleep(1000);

                for (;;);

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread two = new Thread(() ->{
            try {
                System.out.println("线程2");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        one.start();
        two.start();

        one.join();
        two.join();
    }
}

上面这段代码中,首先启动第一个线程,输出线程1 然后休眠,线程2获取执行权,打印线程2 ,之后线程2休眠,线程1获取执行权,然后由于线程1调用了join方法,所以此时主线程阻塞,因为要等线程1执行完毕之后,主线程才会继续执行,但是线程1是个死循环,永远不会结束,所以,这段程序将不会停止,也就是主线程将一直阻塞,two.join也没有执行的机会。

同理,如果A线程中调用B线程的join方法,那么A线程将会被暂时阻塞,直到B线程执行完毕,A线程才会继续,其实对于join,就有点像把多个线程合并成一个单线程来处理