Java 线程间的通信、协作方式 – join 机制,wait-notify 机制

导读:本篇文章讲解 Java 线程间的通信、协作方式 – join 机制,wait-notify 机制,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

Java并发知识体系持续更新:
https://blog.csdn.net/m0_46144826/category_9881831.html

在一般的SSM框架增删改查中,我们很少需要用到多线程协作。(应该是基本用不到)

但是在稍微偏向技术方面,甚至是,自己做点小玩具,想要更加高效时,就需要用到多线程之间的协调通信。

刚开始学习并发,也不知道全不全。。。

wait、notify 机制

首先要介绍的就是,并发中比较特殊的方法。

waitnotify 系列方法是写在 Object 中的, Object 在 JAVA 中的地位,那简直是老祖宗了,除了 Class 外,根本没有其他类有这种地位。。。

所以首先我提出的疑问就是,为什么要把这几个方法写在 Object 中,而不是 Thread 中?

在网上查了半天资料,才发现这居然还是一道号称艰难的面试题。

结论可能会涉及后面的知识,还有一些我都看不懂的,先全部列上:

  1. 首先是,wait 和 notify 都必须在同步中才能生效,这些方法都必须标识同步所属的锁。任意对象都可以作为锁 ,所以将这两方法设计在了 Object 类而不是 Thread 类中。
  2. wait 方法暂停的是持有锁的对象,所以想调用方式为 Object.wait() ,notify 也一样。
  3. wait 和 notify 不仅仅是普通方法或同步工具,更重要的是它们是 Java 中两个线程之间的通信机制。 如果不能通过 Java 关键字(例如 synchronized)实现通信此机制,同时又要确保这个机制对每个对象可用,那么 Object 类则是的合理的声明位置。
  4. 在 Java 中,为了进入代码的临界区,线程需要锁定并等待锁,他们不知道哪些线程持有锁,而只是知道锁被某个线程持有, 并且需要等待以取得锁, 而不是去了解哪个线程在同步块内,并请求它们释放锁。

说实话,大佬的理解我确实看不懂。。。。

Java 线程间的通信、协作方式 - join 机制,wait-notify 机制

同步等待通知 是两个不同的领域,不要把它们看成是相同的或相关的。

同步是提供互斥并确保 Java 类的线程安全,而 wait 和 notify 是两个线程之间的通信机制

好嘞,然后继续。wait() 的三个重载方法中,两个方法调用另一个 native 本地方法。

    //无参方法,默认 wait(0),无限期等待
    public final void wait() throws InterruptedException {
        wait(0);
    }
    
    public final void wait(long timeout, int nanos) throws InterruptedException {
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException("nanosecond timeout value out of range");
        }
        if (nanos >= 500000 || (nanos != 0 && timeout == 0)) {
            timeout++;
        }
        wait(timeout);
    }

最终产生作用的是下面这个本地方法:

    /**
     * 导致当前线程等待,直到另一个线程调用此对象的notify()方法或notifyAll()方法,或指定的时间已过。
     * <p>当前线程必须持有本身对象监视器。
     * <p>此方法使当前线程(称为T)将自身置入等待 set 集合中,然后放弃该对象的所有同步声明。
     *之后线程 T 无法成为线程调度的目标,并且休眠,直到发生四件事情之一:
     * <ul>
     * <li>一些其他线程调用该对象的notify方法,并且线程T恰好被任意选择为被唤醒的线程。
     * <li>某些其他线程调用此对象的notifyAll方法。
     * <li>一些其他线程interrupts线程T。
     * <li>指定的实时数量已经过去,或多或少。 然而,如果timeout为零,则不考虑实时,线程等待直到通知。
     * </ul>
     * 然后从该对象的等待set集合中删除线程T ,并重新启用线程调度。
     * 然后它以通常的方式与其他线程竞争在对象上进行同步的权限;
     * 一旦获得了对象的控制,其对对象的所有同步声明就恢复到现状,也就是在调用wait方法之后的情况。
     * 线程T然后从调用wait方法返回。 因此,从返回wait方法,对象和线程的同步状态T正是因为它是当wait被调用的方法。
     *
     * <p>线程也可以唤醒,而不会被通知,中断或超时,即所谓的虚假唤醒 。
     * 虽然这在实践中很少会发生,但应用程序必须通过测试应该使线程被唤醒的条件来防范,并且如果条件不满足则继续等待。
     * 换句话说,等待应该总是出现在循环中,就像这样:
     * <pre>
     *     synchronized (obj) {
     *         while (<condition does not hold>)
     *             obj.wait(timeout);
     *         ... // Perform action appropriate to condition
     *     }
     * </pre>
     * <p>如果当前线程interrupted任何线程之前或在等待时,那么InterruptedException被抛出。
     * 如上所述,在该对象的锁定状态已恢复之前,不会抛出此异常。
     * <p>请注意, wait方法,因为它将当前线程放入该对象的等待集,仅解锁此对象;
     * 当前线程可以同步的任何其他对象在线程等待时保持锁定。
     * <p>该方法只能由作为该对象的监视器的所有者的线程调用。
     * 有关线程可以成为监视器所有者的方法的说明,请参阅notify方法。
     *
     * @param      timeout   等待的最长时间(以毫秒为单位)。
     * @throws  IllegalArgumentException      如果timeout值为负。
     * @throws  IllegalMonitorStateException  如果当前线程不是此对象的监视器的所有者
     * @throws  InterruptedException 如果任何线程在当前线程等待通知之前或当前线程中断当前线程。 当抛出此异常时,当前线程的中断状态将被清除。
     */
    public final native void wait(long timeout) throws InterruptedException;

这是官方注解的谷歌翻译。

上面的官方文档的每句话都蛮重要的。。。当然最重要的就两点:

  1. 使用 wait 的方法前提: 当前线程必须持有本身对象监视器
  2. 从 wait 唤醒的方式:
    • 其他线程调用该对象的 notify 或 notifyAll 方法。
    • 其他线程 interrupts 此线程。
    • 休眠时间已经过去,线程重新等待调度。如果timeout为零,则不考虑实时,线程等待直到通知。

notify 方法则有以下两个:

    /**
     * 唤醒正在等待对象监视器的单个线程。
     * 如果任何线程正在等待这个对象,其中一个被选择被唤醒。
     * 选择是任意的,并且由实施器判断发生。
     * 线程通过调用wait方法之一等待对象的监视器。
     *
     * <p>唤醒的线程将无法继续,直到当前线程放弃此对象上的锁定为止。
     * 唤醒的线程将以通常的方式与任何其他线程竞争,这些线程可能正在积极地竞争在该对象上进行同步;
     * 例如,唤醒的线程在下一个锁定该对象的线程中没有可靠的权限或缺点。
     *
     * <p>该方法只能由作为该对象的监视器的所有者的线程调用。 线程以三种方式之一成为对象监视器的所有者:
     * <ul>
     * <li>通过执行该对象的同步实例方法。
     * <li>通过执行在对象上synchronized synchronized语句的正文。
     * <li>对于类型为Class,的对象,通过执行该类的同步静态方法。
     * </ul>
     * <p>一次只能有一个线程可以拥有一个对象的显示器。
     * 
     * @throws  IllegalMonitorStateException  如果当前线程不是此对象的监视器的所有者
     */
    public final native void notify();

    /**
     * 唤醒正在等待对象监视器的所有线程。 线程通过调用wait方法之一等待对象的监视器。
     * <p>唤醒的线程将无法继续,直到当前线程释放该对象上的锁。
     * 唤醒的线程将以通常的方式与任何其他线程竞争,这些线程可能正在积极地竞争在该对象上进行同步;
     * 例如,唤醒的线程在下一个锁定该对象的线程中不会有可靠的特权或缺点。
     * <p>该方法只能由作为该对象的监视器的所有者的线程调用。
     * 有关线程可以成为监视器所有者的方法的说明,请参阅notify方法。
     * @throws  IllegalMonitorStateException  如果当前线程不是此对象的监视器的所有者
     */
    public final native void notifyAll();

整个栗子测试下:

public class TestMain {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        WaitNotify example = new WaitNotify();
        executorService.execute(example::waitObj);
        executorService.execute(example::notifyObj);
        executorService.shutdown();
        System.out.println("main 主线程结束");
    }

    public static class WaitNotify {

        public synchronized void notifyObj() {
            System.out.println("notity 调用");
            notifyAll();
        }

        public synchronized void waitObj() {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("wait 结束");
        }
    }
}

若启动多个线程调用 wait() 方法,则如下图:

wait、notify机制示意图

PS: 没有设置守护线程,因此 main 方法将永远不会终结。

这里顺便补充下,sleep()wait() 的区别:

sleep() wait()
Thread 的静态方法 Object 的方法
保留锁 释放锁

join

join 方法的作用是使所属的线程对象x正常执行 run() 方法中的任务,而使当前线程z进行无限期的阻塞,等待线程x销毁后再继续执行线程z后面的代码。

join 方法具有使线程排队运行的作用,有些类似同步的运行效果。

join 与 synchronized 的区别是:join 在内部使用 wait()方法进行等待,而 synchronized 关键字使用的是 JVM 底层,使用“对象监视器”原理作为同步。

先来看下 join 方法的源码,其他两个无参和带参方法,最终调用的都是这个 synchronized 方法:

    /**
     * @param  millis 等待时间(以毫秒为单位)
     * @throws  IllegalArgumentException 如果{@code millis}的值为负
     * @throws  InterruptedException 如果有任何线程中断了当前线程,抛出此异常时,线程的中断状态将被清除。
     */
    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) {
            //如果线程未死亡,则循环调用 wait
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                //第一次进入,now 为0,等待 millis 毫秒
                //第二次进入,now 为已经等待时间,delay小于等于0时跳出
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

从源码中可以发现, join 最后还是基于 wait 方法实现的。

先看个例子,看看如何工作,再画图:

public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(2000);
                System.out.println("join 线程的 run 方法");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        });
        thread.start();

        for (int i = 0; i < 10; i++) {
            if (i == 5){
                thread.join(1999);
            }else {
                System.out.println(System.currentTimeMillis() + ", main 线程循环中:" + i);
            }
        }
    }

运行结果,省略前后:

...
1596276428638, main 线程循环中:4
1596276430640, main 线程循环中:6
join 线程的 run 方法
1596276430640, main 线程循环中:7
...

再放张图,大概示意下工作流程:

join 机制线程示意图

await、signal

java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。

这种方式可以在一个 Lock 对象里面可以创建多个Condition(即对象监视器)实例,线程对象可以注册在指定的Condition中,从而可以有选择性地对指定线程进行通知,所以更加灵活。

这种方式先贴一个大佬的例子,内容有点多,以后再说,我感觉以后肯定会讲到,应该会有专门一篇。

class AwaitTest {

    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void before() {
        lock.lock();
        try {
            System.out.println("before");
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public void after() {
        lock.lock();
        try {
            condition.await();
            System.out.println("after");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        AwaitTest example = new AwaitTest();
        executorService.execute(example::after);
        executorService.execute(example::before);
        executorService.shutdown();
    }


}

参考文章

https://www.pdai.tech/md/java/thread/java-thread-x-thread-basic.html

https://segmentfault.com/a/1190000019962661

https://www.jianshu.com/p/beb5413c5ce6

https://www.cnblogs.com/Donnnnnn/p/7234934.html

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由半码博客整理,本文链接:https://www.bmabk.com/index.php/post/10303.html

(0)
小半的头像小半

相关推荐

半码博客——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!