liuxiaoshui
发布于 2023-11-23 / 258 阅读
0
0

Thread之sleep、join、yield、wait、notify

一、sleep方法


线程释放CPU进入休眠,但不会释放锁(synchronized),释放CPU,不释放锁

这里面有个比较经典的用法,代码中循环太快,导致年轻代的GC频繁或者GC时间久,可以通过Thread.sleep(0)释放CPU,让GC线程去执行回收

经典用法:线程批任务导致cpu占比很高,通过Thread.sleep(0)可以降低CPU占比

补充:

Thread.sleep(0)的作用

  1. Thread.Sleep(0)作用,就是“触发操作系统立刻重新进行一次CPU竞争”。竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权。这也是我们在大循环里面经常会写一句Thread.Sleep(0) ,因为这样就给了其他线程比如Paint线程获得CPU控制权的权力,这样界面就不会假死在那里。

  2. Thread.sleep(0) 效果跟Thread.yield()效果都是一样的。但是如果 Thread.sleep(long millis)的参数 millis大于0,那么JVM底层调用的是 os::sleep,等于0调用的是 os::yield();

关于sleep()方法和yield()方法的区别如下。

  1. sleep()方法暂停 当前线程后,会给其他线程执行机会,不会理会其他线程的优先级:但yield()方法只会给优先级相同,或优先级更高的线程执行机会。

  2. sleep()方法 会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态;而yield()不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态。因此完全有可能某个线程调用yield()方法暂停之后,立即再次获得处理器资源被执行。

  3. sleep()方法声明抛出了InterruptedException异常,所以调用sleep()方法时要么捕捉该异常,要么显式声明抛出该异常:而yield()方法则没有声明抛出任何异常。

  4. sleep()方法比yield()方法有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。

二、yield方法(很少用)

很少使用,表示当前线程愿意让出CPU执行器的当前使用权,释放CPU,不释放锁,但是调度器可以自由忽略这个提示。

Yield是一种让多线程的执行进度 尽可能一致的方案,比如有4个线程同时执行一样的算法,但是只有两个线程能同时运行,通过Yield,可以让4个线程尽可能在差不多的时间完成

Thread.yield()某种程度上效果等同于Thread.sleep(0)

三、join方法,控制多个线程的执行顺序

常用操作:线程B等线程A执行完成之后再执行,join底层调用的是wait(),会释放CPU,不会释放对象锁。

public class Test {

    public static void main(String[] args) {
        Thread A = new Thread(new Runnable() {
            @Override
            public void run() {
                print("A");
            }
        });
        Thread B = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("B 开始等待 A");
                try {
                    A.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                print("B");
            }
        });
        B.start();
        A.start();
    }


    private static void print(String b) {
        System.out.println(b+".............1");
        System.out.println(b+".............2");
        System.out.println(b+".............3");

    }
}
输出:
B 开始等待 A
A.............1
A.............2
A.............3
B.............1
B.............2
B.............3

四、wait()和notify()以及notifyAll() (对象锁)

常用操作:两个线程按照指定方式有序交叉运行,wait会释放synchronized锁,释放CPU

经典用法:dubbo底层 调用netty实现RPC调用,dubbo线程调用netty线程之后,进入wait等待状态,netty线程拿到RPC结果后通过notify对dubbo线程进行唤醒

    public static void main(String[] args) {

        Object lock = new Object();
        Thread A = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    System.out.println("A 1");
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("A 2");
                    System.out.println("A 3");
                }
            }
        });
        Thread B = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    System.out.println("B 1");
                    System.out.println("B 2");
                    System.out.println("B 3");
                    lock.notify();
                }
            }
        });

        A.start();
        B.start();


    }

输出:

A 1
B 1
B 2
B 3
A 2
A 3

wait 和 notify 这个为什么要在 synchronized 代码块中?

wait 和 notify 是 Java 中用于线程间通信的方法,它们必须与 synchronized 关键字一起使用,否则会抛出 IllegalMonitorStateException 异常。这是因为 wait 和 notify 操作涉及到线程的挂起和唤醒,需要一种同步机制来保证条件的检查和操作的执行是互斥的,避免出现执行混乱的问题。synchronized 关键字可以提供这种同步机制,它可以保证同一时刻只有一个线程持有对象的锁,从而实现线程间的协作。具体的原理和示例,你可以参考以下的网页:


补充:这里有篇写挺好

https://blog.csdn.net/tom540066931/article/details/81088786

question:

不过有一点需要注意,这里的join只调用了wait方法,却没有对应的notify方法,原因是Thread的start方法中做了相应的处理,所以当join的线程执行完成以后,会自动唤醒主线程继续往下执行。

ans:

join 方法的作用可以概括为“线程插队”,即阻塞当前线程,让其它线程先执行,等其它线程执行完或超时,当前线程再恢复执行。

join 方法是用 wait 方法实现的,所以 wait 方法可以被 interrupt,join 方法自然也可以。

当然 join 方法也必须获得线程对象的锁。

可以看下它的实现:

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;
            }
        }
    }

核心逻辑也就是下面这三行代码(else 里面增加了对超时时间的控制,本质一样):

while (isAlive()) {
                wait(0);
            }

wait(0) 等同于 wait() ,即没有超时时间。

这三行代码的意思就是: 如果 join 进来的线程还活着(即已经调了 start 方法但还没有执行完成),就一直等。

不用担心这里的 wait 会永远等待下去,因为线程终止的时候,会调用 this.notifyAll 方法。

可以写几行代码测试一下:

import java.util.concurrent.TimeUnit;

public class ThreadTerminatedTest {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println("thread begin");
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("thread end");
        });
        thread.start();
        synchronized (thread) {
            thread.wait();
        }
        System.out.println("main end");
    }
}

在 Thread 对象上 wait。跑一下可以发现 thread 执行完之后,wait 是会被唤醒的。

这几种线程等待唤醒机制(面试必问)你知道吗?

三种让线程等待和唤醒的方法

  • 方式一:使用 Object 中的 wait() 方法让线程等待,使用 Object 中的 notify() 方法唤醒线程

  • 方式二:使用 JUC 包中 Condition 的 await() 方法让线程等待,使用 signal() 方法唤醒线程

  • 方式三:LockSupport 类可以阻塞当前线程以及唤醒指定被阻塞的线程。


评论