你知道wait/notify的这些知识点吗?

在Java的Object类中有2个咱们不怎么经常使用(框架中用的更多)的方法:wait()与notify()或notfiyAll(),这两个方法主要用于多线程间的协同处理,即控制线程之间的等待、通知、切换及唤醒。html

首先了解下线程有哪几种状态,Java的Thread.State中定义了线程的6种状态,分别以下:java

  1. NEW 未启动的,不会出如今dump文件中(能够经过jstack命令查看线程堆栈)
  2. RUNNABLE 正在JVM中执行的
  3. BLOCKED 被阻塞,等待获取监视器锁进入synchronized代码块或者在调用Object.wait以后从新进入synchronized代码块
  4. WAITING 无限期等待另外一个线程执行特定动做后唤醒它,也就是调用Object.wait后会等待拥有同一个监视器锁的线程调用notify/notifyAll来进行唤醒
  5. TIMED_WAITING 有时限的等待另外一个线程执行特定动做
  6. TERMINATED 已经完成了执行

从操做系统层面上来说,一个进程从建立到消亡期间,最多见的进程状态有如下几种 新建态 : 从程序映像到进程映像的转变,尚未加入到就绪队列中 就绪态 : 进程运行已万事俱备,正等待调度执行 运行态 : 进程指令正在被执行 阻塞态 : 进程正在等待一个时间操做完成,例如I/O操做 完成态 : 进程运行结束,它的资源已经被释放,供其余活动进程使用linux

接下来咱们分析下面的代码git

public class WaitNotify {

  public static void main(String[] args) {

    Object lock = new Object();
    
    // thread1
    new Thread(() -> {

        System.out.println("thread1 is ready");

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {

        }
        synchronized (lock) {
            lock.notify();
            System.out.println("thread1 is notify,but not exit synchronized");
            System.out.println("thread1 is exit synchronized");
        }


    }).start();

    // thread2
    new Thread(() -> {
        System.out.println("thread2 is ready");

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {

        }
        synchronized (lock) {
            try {
                System.out.println("thread2 is waiting");
                lock.wait();
                System.out.println("thread2 is awake");
            } catch (InterruptedException e) {
            }
        }
    }).start();
  }
}
复制代码

上面的代码运行结果以下github

thread1 is ready
thread2 is ready
thread2 is waiting
thread1 is notify,but not exit synchronized
thread 1 is exit synchronized
thread2 is awake
复制代码

看到这里你会发现平平无奇,好像也没什么特殊的事情,可是若是深刻分析下,就会发现如下的问题api

  1. 为什么调用wait或者notify必定要加synchronized,不加行不行?
  2. thread2中调用了wait后,当前线程还未退出同步代码块,其余线程(thread1)能进入同步块吗?
  3. 为什么调用wait()有可能抛出InterruptedException异常
  4. 调用notify/notifyAll后等待中的线程会马上唤醒吗?
  5. 调用notify/notifyAll是随机从等待线程队列中取一个或者按某种规律取一个来执行?
  6. wait的线程是否会影响系统性能?

针对上面的问题,咱们逐个分析下bash

为什么调用wait或者notify必定要加synchronized,不加行不行?

若是你不加,你会获得下面的异常多线程

Exception in thread "main" java.lang.IllegalMonitorStateException
复制代码

JVM源代码中首先会检查当前线程是否持有锁,若是没有持有则抛出异常oracle

其次为何要加,也有比较普遍的讨论,首先wait/notify是为了线程间通讯的,为了这个通讯过程不被打断,须要保证wait/notify这个总体代码块的原子性,因此须要经过synchronized来加锁。框架

thread2中调用了wait后,当前线程还未退出同步代码块,其余线程(thread1)能进入同步块吗?

wait在处理过程当中会临时释放同步锁(若是不释放其余线程没有机会抢),不过须要注意的是当其余线程调用notify唤起这个线程的时候,在wait方法退出以前会从新获取这把锁,只有获取了这把锁才会继续执行,这也和咱们的结果相符合,输出了thread2 is awake, 其实想一想也容易理解,synchronized的代码其实是被被monitorenter和monitorexit包围起来的。当咱们调用wait的时候,会释放锁,调用monitorexit的时候也会释放锁,那么当thread2被唤醒的时候必然从新获取到了锁(objectMonitor::enter)。

其实从jdk源代码的ObjectMonitor::wait方法能够一窥究竟,首先会放弃已经抢到的锁(exit(self)),而放弃锁的前提是获取到锁

而在notify方法中会选取一个线程得到cpu执行权,在去竞争锁,若是没有竞争到则会进入休眠。

若是调用的wait(200)这种代码,那么会在200ms后将线程从waiting set中移除并容许其从新竞争锁,须要注意的是notify方法并不会释放所持有的monitor

为什么调用wait()有可能抛出InterruptedException异常

当咱们调用了某个线程的interrupt方法,对应的线程会抛出这个异常,wait方法也不但愿去破坏这种规则,所以就算当前线程由于wait一直在阻塞。当某个线程但愿它起来继续执行的时候,它仍是得从阻塞态恢复过来,所以wait方法被唤醒起来的时候会去检测这个状态,当有线程interrupt了它的时候,它就会抛出这个异常从阻塞状态恢复过来。

这里有两点要注意:

  1. 若是被interrupt的线程只是建立了,并无start,那等他start以后进入wait态以后也是不能会恢复的
  2. 若是被interrupt的线程已经start了,在进入wait以前,若是有线程调用了其interrupt方法,那这个wait等于什么都没作,会直接跳出来,不会阻塞,下面的代码演示了这种状况
Thread thread2 = new Thread(() -> {
    System.out.println("thread2 is ready");

    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {

    }
    synchronized (lock) {
        try {
            System.out.println("thread2 is waiting");
            lock.wait();
            System.out.println("thread2 is awake");
        } catch (InterruptedException e) {
            System.out.println(e);
        }
    }
});

  // main
  thread2.start();

  Thread.sleep(1000);
  thread2.interrupt();

复制代码

上面的代码输出结果以下

thread2 is ready
thread2 is waiting
java.lang.InterruptedException
复制代码

调用notify/notifyAll后等待中的线程会马上唤醒吗?

hotspot真正的实现是退出同步代码块的时候才会去真正唤醒对应的线程,不过这个也是个默认策略,也能够改的,在notify以后立马唤醒相关线程。 这个也可从jdk源代码的objectMonitor类objectMonitor::notify方法中看到.在调用notify时的默认策略是Policy == 2(这个值是源码中的初值,能够经过-XX:SyncKnobs来设置)

其实对于Policy(一、二、三、4)都是将objectMonitor的ObjectWaiter集合中取出一个等待线程,放入到_EntryList(blocked线程集合,能够参与下次抢锁),只是放入_EntryList的策略不同,体现为唤醒wait线程的规则不同。

对于默认策略notify在将一个等待线程放入阻塞线程集合以后就退出,由于同步块尚未执行完monitorexit,锁其实还未释放,因此在打印出“thread1 is exit synchronized!”的时候,thread2线程仍是blocked状态(由于thread1尚未退出同步块)。

这里能够发现,对于不在Policy中的状况,会直接将一个ObjectWaiter进行unpark唤醒操做,可是被唤醒的线程是否当即获取到了锁呢?答案是否认的。

调用notify/notifyAll是随机从等待线程队列中取一个或者按某种规律取一个来执行?

咱们本身实现可能一个for循环就搞定了,不过在jvm里实现没这么简单,而是借助了monitor_exit,上面我提到了当某个线程从wait状态恢复出来的时候,要先获取锁,而后再退出同步块,因此notifyAll的实现是调用notify的线程在退出其同步块的时候唤醒起最后一个进入wait状态的线程,而后这个线程退出同步块的时候继续唤醒其倒数第二个进入wait状态的线程,依次类推,一样这是一个策略的问题,jvm里提供了挨个直接唤醒线程的参数,这里要分状况:

  1. 若是是经过notify来唤起的线程,那先进入wait的线程会先被唤起来
  2. 若是是经过nootifyAll唤起的线程,默认状况是最后进入的会先被唤起来,即LIFO的策略

wait的线程是否会影响系统性能?

这个或许是你们比较关心的话题,由于关乎系统性能问题,wait/nofity是经过jvm里的park/unpark机制来实现的,在linux下这种机制又是经过pthread_cond_wait/pthread_cond_signal来玩的,所以当线程进入到wait状态的时候实际上是会放弃cpu的,也就是说这类线程是不会占用cpu资源,也不会影响系统加载。

什么是监视器(monitor)

Java中每个对象均可以成为一个监视器(Monitor), 该Monitor由一个锁(lock), 一个等待队列(waiting queue ), 一个入口队列( entry queue)组成. 对于一个对象的方法, 若是没有synchonized关键字, 该方法能够被任意数量的线程,在任意时刻调用。 对于添加了synchronized关键字的方法,任意时刻只能被惟一的一个得到了对象实例锁的线程调用。

进入区(Entry Set): 表示线程经过 synchronized要求得到对象锁,若是获取到了,则成为拥有者,若是没有获取到在在进入区等待,直到其余线程释放锁以后再去竞争(谁获取到则根据)

拥有者(Owner): 表示线程获取到了对象锁,能够执行synchronized包围的代码了

等待区(Wait Set): 表示线程调用了wait方法,此时释放了持有的对象锁,等待被唤醒(谁被唤醒取得监视器锁由jvm决定)

关于sleep

它是一个静态方法,通常的调用方式是Thread.sleep(2000),表示让当前线程休眠2000ms,并不会让出监视器,这一点须要注意。

你的关注是对我最大的支持。

相关文章
相关标签/搜索