【多线程】一文图解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和锁就可以继续执行。

总结

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

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

相关文章
|
3月前
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
76 12
|
2月前
|
Java 调度
|
2月前
|
Java API 调度
【JavaEE】——多线程(join阻塞,计算,引用,状态)
【JavaEE】——多线程,join,sleep引起的线程阻塞,多线程提升计算效率,如何获取线程的引用和状态
|
3月前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
100 9
|
3月前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
55 3
|
4月前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
44 1
|
4月前
|
Java
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅。它们用于线程间通信,使线程能够协作完成任务。通过这些方法,生产者和消费者线程可以高效地管理共享资源,确保程序的有序运行。正确使用这些方法需要遵循同步规则,避免虚假唤醒等问题。示例代码展示了如何在生产者-消费者模型中使用`wait()`和`notify()`。
49 1
|
3天前
|
Python
python3多线程中使用线程睡眠
本文详细介绍了Python3多线程编程中使用线程睡眠的基本方法和应用场景。通过 `time.sleep()`函数,可以使线程暂停执行一段指定的时间,从而控制线程的执行节奏。通过实际示例演示了如何在多线程中使用线程睡眠来实现计数器和下载器功能。希望本文能帮助您更好地理解和应用Python多线程编程,提高程序的并发能力和执行效率。
32 20
|
9天前
|
安全 Java C#
Unity多线程使用(线程池)
在C#中使用线程池需引用`System.Threading`。创建单个线程时,务必在Unity程序停止前关闭线程(如使用`Thread.Abort()`),否则可能导致崩溃。示例代码展示了如何创建和管理线程,确保在线程中执行任务并在主线程中处理结果。完整代码包括线程池队列、主线程检查及线程安全的操作队列管理,确保多线程操作的稳定性和安全性。
|
2月前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
79 1