2018 元旦快乐。java
摘要:面试
今天咱们要学习或者说分析的是 Object 类中的 wait notify 这两个方法,其实说是两个方法,这两个方法包括他们的重载方法一共有5个,而Object 类中一共才 12 个方法,可见这2个方法的重要性。咱们先看看 JDK 中的代码:算法
public final native void notify();
public final native void notifyAll();
public final void wait() throws InterruptedException {
wait(0);
}
public final native void wait(long timeout) throws InterruptedException;
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 > 0) {
timeout++;
}
wait(timeout);
}
复制代码
就是这五个方法。其中有3个方法是 native 的,也就是由虚拟机本地的c代码执行的。有2个 wait 重载方法最终仍是调用了 wait(long) 方法。编程
首先仍是 know how。来一个最简单的例子,看看如何使用这两个方法。缓存
package cn.think.in.java.two;
import java.util.concurrent.TimeUnit;
public class WaitNotify {
final static Object lock = new Object();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程 A 等待拿锁");
synchronized (lock) {
try {
System.out.println("线程 A 拿到锁了");
TimeUnit.SECONDS.sleep(1);
System.out.println("线程 A 开始等待并放弃锁");
lock.wait();
System.out.println("被通知能够继续执行 则 继续运行至结束");
} catch (InterruptedException e) {
}
}
}
}, "线程 A").start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程 B 等待锁");
synchronized (lock) {
System.out.println("线程 B 拿到锁了");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
}
lock.notify();
System.out.println("线程 B 随机通知 Lock 对象的某个线程");
}
}
}, "线程 B").start();
}
}
复制代码
运行结果:多线程
线程 A 等待拿锁 线程 B 等待锁 线程 A 拿到锁了 线程 A 开始等待并放弃锁 线程 B 拿到锁了 线程 B 随机通知 Lock 对象的某个线程 被通知能够继续执行 则 继续运行至结束并发
在上面的代码中,线程 A 和 B 都会抢这个 lock 对象的锁,A 的运气比较好(也可能使 B 拿到锁),他先拿到了锁,而后调用了 wait 方法,放弃了锁,并挂起了本身,这个时候等待锁的 B 就拿到了锁,而后通知了A,可是请注意,通知完毕以后,B 线程并无执行完同步代码块中的代码,所以,A 仍是拿不到锁的,所以没法运行,等到B线程执行完毕,出了同步块,这个时候 A 线程才被激活得以继续执行。dom
使用 wait 方法和 notify 方法可使 2 个无关的线程进行通讯。也就是面试题中常提到的线程之间如何通讯。ide
若是没有 wait 方法和 noitfy 方法,咱们如何让两个线程通讯呢?简单的办法就是让某个线程循环去检查某个标记变量,好比:源码分析
while (value != flag) {
Thread.sleep(1000);
}
doSomeing();
复制代码
上面的这段代码在条件不知足使就睡眠一段时间,这样作到目的是防止过快的”无效尝试“,这种方式看似可以实现所需的功能,可是却存在以下问题:
可是有了Java 自带的 wait 方法 和 notify 方法,一切迎刃而解。官方说法是等待/通知机制。一个线程在等待,另外一个线程能够通知这个线程,实现了线程之间的通讯。
注意,这两个方法的使用必须是在 synchroized 同步块中,而且在当前对象的同步块中,若是在 A 对象的方法中调用 B 对象的 wait 或者 notify 方法,虚拟机会抛出 IllegalMonitorStateException,非法的监视器异常,由于你这个线程持有的监视器和你调用的监视器的不是一个对象。
那么为何这两个方法必定要在同步块中呢?
这里要说一个专业名词:竞态条件。什么是竞太条件呢?
当两个线程竞争同一资源时,若是对资源的访问顺序敏感,就称存在竞态条件。
竞态条件会致使程序在并发状况下出现一些bugs。多线程对一些资源的竞争的时候就会产生竞态条件,若是首先要执行的程序竞争失败排到后面执行了,那么整个程序就会出现一些不肯定的bugs。这种bugs很难发现并且会重复出现,这是由于线程间会随机竞争。
假设有2个线程,分别是生产者和消费者,他们有各自的任务。
1.1生产者检查条件(如缓存满了)-> 1.2生产者必须等待 2.1消费者消费了一个单位的缓存 -> 2.2从新设置了条件(如缓存没满) -> 2.3调用notifyAll()唤醒生产者
咱们但愿的顺序是: 1.1->1.2->2.1->2.2->2.3 可是因为CPU执行是随机的,可能会致使 2.3 先执行,1.2 后执行,这样就会致使生产者永远也醒不过来了!
因此咱们必须对流程进行管理,也就是同步,经过在同步块中并结合 wait 和 notify 方法,咱们能够手动对线程的执行顺序进行调整。
虽然不少书中都不建议咱们直接使用 notify 和 wait 方法进行并发编程,但仍然须要咱们重点掌握。楼主写了一个简单的生产者消费者例子:
public class Queue {
final int num;
final List<String> list;
boolean isFull = false;
boolean isEmpty = true;
public Queue(int num) {
this.num = num;
this.list = new ArrayList<>();
}
public synchronized void put(String value) {
try {
if (isFull) {
System.out.println("putThread 暂停了,让出了锁");
this.wait();
System.out.println("putThread 被唤醒了,拿到了锁");
}
list.add(value);
System.out.println("putThread 放入了" + value);
if (list.size() >= num) {
isFull = true;
}
if (isEmpty) {
isEmpty = false;
System.out.println("putThread 通知 getThread");
this.notify();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized String get(int index) {
try {
if (isEmpty) {
System.err.println("getThread 暂停了,并让出了锁");
this.wait();
System.err.println("getThread 被唤醒了,拿到了锁");
}
String value = list.get(index);
System.err.println("getThread 获取到了" + value);
list.remove(index);
Random random = new Random();
int randomInt = random.nextInt(5);
if (randomInt == 1) {
System.err.println("随机数等于1, 清空集合");
list.clear();
}
if (getSize() < num) {
if (getSize() == 0) {
isEmpty = true;
}
if (isFull) {
isFull = false;
System.err.println("getThread 通知 putThread 能够添加了");
Thread.sleep(10);
this.notify();
}
}
return value;
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
public int getSize() {
return list.size();
}
复制代码
class PutThread implements Runnable {
Queue queue;
public PutThread(Queue queue) {
this.queue = queue;
}
@Override
public void run() {
int i = 0;
for (; ; ) {
i++;
queue.put(i + "号");
}
}
}
复制代码
class GetThread implements Runnable {
Queue queue;
public GetThread(Queue queue) {
this.queue = queue;
}
@Override
public void run() {
for (; ; ) {
for (int i = 0; i < queue.getSize(); i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String value = queue.get(i);
}
}
}
}
复制代码
你们有兴趣能够跑跑看,可以加深这两个方法的理解,实际上,JDK 内部的阻塞队列也是相似这种实现,可是,不是用的 synchronized ,而是使用的重入锁。
基本上经典的生产者消费者模式的有着以下规则:
等待方遵循以下规则:
对应的伪代码入下:
synchroize( 对象 ){
while(条件不知足){
对象.wait();
}
对应的处理逻辑......
}
复制代码
通知方遵循以下规则:
对应的伪代码以下:
synchronized(对象){
改变条件
对象.notifyAll();
}
复制代码
知道了如何使用,就得知道他的原理究竟是什么?
首先咱们看,使用这两个方法的顺序通常是什么?
从上述细节能够看到,等待/通知机制依托于同步机制,其目的就是确保等待线程从 wait 方法返回后可以感知到通知线程对变量作出的修改。
该图描述了上面的步骤:
WaitThread 得到了对象的锁,调用对象的 wait 方法,放弃了锁,进入的等待队列,而后 NotifyThread 拿到了对象的锁,而后调用对象的 notify 方法,将 WatiThread 移动到同步队列中,最后,NotifyThread 执行完毕,释放锁, WaitThread 再次得到锁并从 wait 方法返回继续执行。
到这里,关于应用层面的 wait 和 notify 基本就差很少了,后面的是关于虚拟机层面的抛砖引玉,涉及到 Java 的内置锁实现,synchronized 关键字底层实现,JVM 源码。算是本文的扩展吧。
注意:咱们看到图中出现了 Monitor 这个词,也就是监视器,实际上,在 JDK 的注释中,也有 The current thread must own this object's monitor 这句话,当前线程必须拥有该对象的监视器。
若是咱们编译这段含有 synchronized 关键字的代码,就会发现有一段代码被 monitorenter 指令和 monitorexit 指令括住了,这就是 synchronized 在编译期间作的事情,那么,在字节码被执行的时侯,该指令对应的 c 代码将会被执行。这里,咱们必须打住,这里已经开始涉及到 synchronized 的相关原理了,本篇文章不会讨论这个。
wait noitfy 的答案都在 Java HotSpot 虚拟机的 C 代码中。但 R 大告诉咱们不要轻易阅读虚拟机源码,众多细节可能会掩盖抽象,致使学习效率不高。若是同窗们有兴趣,有大神写了3篇文章专门从 HotSpot 中解析源码,地址:
Java的wait()、notify()学习三部曲之一:JVM源码分析, Java的wait()、notify()学习三部曲之二:修改JVM源码看参数, Java的wait()、notify()学习三部曲之三:修改JVM源码控制抢锁顺序, 还有狼哥的 JVM源码分析之Object.wait/notify实现.
上面四篇文章都从 JVM 的源码层面解析了 wait ,notify 的实现原理,很是清楚。
好了,关于 wait noitfy 的使用和基本原理就介绍到这里,不知道你们发现没有,并发和虚拟机高度相关。所以,能够说,学习并发的过程就是学习虚拟机的过程。而阅读虚拟机里的 openjdk 代码让人头大,但无论怎么样,丑媳妇早晚见公婆,openjdk 代码是必定要看的,加油!!!!