【多线程】一文图解wait()、notify()、join()源码

简介: 这一篇我们主要是对wait()、notify()、join()进行图解,可能有些粗糙,不足之处多多指出。

前言

大家好,我是小郭,这一篇我们主要是对wait()、notify()、join()进行图解,可能有些粗糙,不足之处多多指出。

概要

  1. wait()方法
  2. notify()方法
  3. join()方法

我们先对Object.wait()进行一波分析。

接着上一篇留下的问题

  1. 为什么调用Object.wait必须持有对象锁?
  2. Object.wait()被挂起后,是否会释放当前锁,让出CPU?

我们先来回答第一个问题

通过锁的原理,知道javap生成的字节码包含"monitorenter" 和"monitorexit",这里先不对锁进行扩展,我们先知道有这么一个东西就行。

这也是为什么wait需要先获取锁,才能获得monitor对象。

1. wait()方法

HotSpot虚拟机中,monitor采用ObjectMonitor 实现

//ObjectMonitor的对象的结构体
ObjectMonitor::
ObjectMonitor() {
    _header = NULL;
    _count = 0;//用来记录该线程获取锁的次数
    _waiters = 0, 
    _recursions = 0;//锁的重入次数
    _object = NULL;
    _owner = NULL; //指向持有ObjectMonitor对象的线程
    _WaitSet = NULL;//存放处于wait状态的线程队列
    _WaitSetLock = 0;
    _Responsible = NULL;
    _succ = NULL;
    _cxq = NULL;
    FreeNext = NULL;
    _EntryList = NULL;//存放处于等待锁block状态的线程队列
    _SpinFreq = 0;
    _SpinClock = 0;
    OwnerIsThread = 0;
}

继续往下看,通过wait接口的解释来回答第二个问题

This method causes the current thread (call it T) toplace itself in the wait set for this object and then to relinquish any and all synchronization claims on this object. 主要就是说,将对象放入waitSet中,然后放弃所有的同步声明,意思就是让出cpu。

在下才疏学浅,画了一个很粗糙的流程图😆,如果有不对的请求指出。

网络异常,图片无法展示
|
总结一下上面的流程

  1. 获取锁的monitor对象。
  2. 检测当前线程对象是否获取锁。
  3. 创建ObjectWaiter,将其状态设置为TS_WAIT。
  4. 操作_WaitSet链表,将当前的node节点尾部插入到队列中。
  5. 调用Exit()方法,退出monitor,同时释放该锁,让出CPU。
  6. 调用Park()方法,将线程挂起。
  7. 当ObjectWaiter状态为TS_WAIT,WaitSet移除当前node节点,修改状态为TS_RUN。
  8. 调用Enter(Self),重新抢占该锁。
  9. 退出当前等待monitor。

2. notify()方法

趁热打铁,我们再对Object.notify()进行一波分析。

网络异常,图片无法展示
|

总结一下上面的流程

  1. 线程A在wait() 后被加入了_WaitSet队列中。
  2. 线程C被线程B启动后竞争锁失败,被加入到_cxq队列的首位。
  3. 线程B在notify()时,从_WaitSet中取出第一个,根据Policy的不同,将这个线程放入_EntryList或者_cxq队列中的起始或末尾位置。
  4. 根据QMode的不同,将ObjectWaiter从_cxq或者_EntryList中取出后唤醒。

3. join()方法

可以看见join方法的核心还是wait,join方法利用了synchronized来修饰,就是因为wait方法必须获取锁。

Thread.join()
//等待该线程结束
public final void join() throws InterruptedException {
    join(0);
}
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;
        }
    }
}

我们来通过实例看一下join的使用

static class CreateRunable implements Runnable {
    public CreateRunable(int i) {
        this.i = i;
    }
    private int i;
    public int getI() {
        return i;
    }
    public void setI(int i) {
        this.i = i;
    }
    @Override
    public  void run() {
        synchronized (this){
            System.out.println("Runable接口,实现线程"+i);
        }
    }
}
public static void main(String[] args) throws InterruptedException {
    for (int i = 0;i< 20 ;i++){
        Thread createThread = new Thread(new CreateRunable(i));
        createThread.start();
        createThread.join();
    }
    System.out.println("mian阻塞最后执行");
}

网络异常,图片无法展示
|

通过一个简单的流程图,就可以看到调用的过程

synchronized修饰在方法层,相当于synchronized(this),也就是createThread本身的实例。

主线程会持有这个对象的锁,然后去调用wait阻塞,谁调用谁阻塞,所以造成了主线程的阻塞,子线程在结束后会调用

nitifyAll()去唤醒主线程,主线程只要获取到了cpu和锁就可以继续执行。

总结

上面三个方法通过简单的流程图来描述源码的过程,我们可以看到主要是通过对锁的抢占来实现的线程等待和释放。

不足之处,希望大家能够多多指出,及时改正~

相关文章
|
24天前
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
51 12
|
1月前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
64 9
|
1月前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
39 3
|
2月前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
24 1
|
2月前
|
Java
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅。它们用于线程间通信,使线程能够协作完成任务。通过这些方法,生产者和消费者线程可以高效地管理共享资源,确保程序的有序运行。正确使用这些方法需要遵循同步规则,避免虚假唤醒等问题。示例代码展示了如何在生产者-消费者模型中使用`wait()`和`notify()`。
32 1
|
2月前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
52 1
|
1天前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
12 1
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
58 1
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
31 3
|
2月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
24 2