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

4,464 阅读3分钟

前言

大家好,我是小郭,这一篇我们主要是对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 <var>T</var>) 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和锁就可以继续执行。

总结

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

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