wait与notify的API以及解释
public final void wait() throws InterruptedException {
wait(0);
}
public final void wait(long timeout, int nanos) throws InterruptedException {}
public final native void wait(long timeout) throws InterruptedException;
public final native void notify();
public final native void notifyAll();
这几个方法都是Object的方法,Object在Java中是万物之母,也就是说,所有的类都会有这几个方法。这时候我们要来理解一下锁的概念了。
在开始wait的讲解之前,先了解一下Java的锁相关的东西做个铺垫。
我们知道,一个共享数据被多个线程同时操作时需要格外注意,因为可能会有线程同步的问题,一个很常用的方法就是当多线程操作同一共享数据的时候,我们会选择给这个操作过程加锁从而保证操作的原子性。我们使用synchronized()为操作加锁的时候需要传递一个参数,这个参数其实就是一个对象,当然如果是对一个静态方法加synchronized的时候,锁对象是类本身。
我们来看一下对象的内存结构:
1.对象是存储在堆内存的
2.对象中包含的数据主要有:对象头,实例变量,填充数据
3.对象头中又保存了锁的标记位和一个指向monitor对象起始地址的引用,这个Monitor对象中,包含了_EntryList,_Owner,_WaitSet这三个部分。
下面我们对这个Monitor对象详细说明:
monitor是由ObjectMonitor实现的ObjectMonitor中有两个队列分别是_EntryList、_WaitSet,用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装成ObjectWaiter对象)。还有一个_Owner对象指向了持有ObjectMonitor对象的线程。当多个线程同时访问一段同步代码时,首先线程会先进入_EntryList这个入口列表,当某一线程得到执行并且获取到对象的monitor后进入_Owner区域并且此时_Owner会指向该线程,同时monitor中有一个计数器count+1,若紧接着该线程调用了wait()方法,那么这个线程会释放当前持有的monitor也就是说_Owner变量会恢复为null,count-1,同时这个线程会进入_WaitSet这个集合中等待某个线程唤醒它。如果这个线程被唤醒,那么它需要重新获取该monitor也就是说该对象的_Owner重新指向它它才能得到执行。当然如果该线程并没有调用wait()方法那么当这个线程执行完毕的时候,也一样会释放monitor也就是说对象的_Owner对象会恢复为null并且count-1,以便其他线程获取该monitor锁。
以上参考了:
https://blog.csdn.net/kking_edc/article/details/108382333
看完以上讲解之后,其实我们对wait()和notify()应该有一定的概念了。现在我们回想一下,我们调用某个锁对象的wait()方法其实是想把这个线程先阻塞住,等到条件合适再继续执行。那么依照上面的分析,其实是把该线程放到该锁对象的_WaitSet集合中等待重新执行。
接下来我们来写一些代码用一用这俩方法:
public Object lock = new Object();
public void main() {
Thread thread1 = new Thread(() -> {
System.out.println("执行" + Thread.currentThread().getName() + "线程");
synchronized (lock) {
try {
System.out.println("将" + Thread.currentThread().getName() + "阻塞在lock对象上");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "线程执行完了");
}, "thread1");
Thread thread2 = new Thread(() -> {
try {
Thread.sleep(3000L);
synchronized (lock) {
lock.notify();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread2");
thread1.start();
thread2.start();
}
输出结果:
执行thread1线程
将thread1阻塞在lock对象上
thread1线程执行完了
上面例子中,分别定义2个线程thread1和thread2,当thread1开始执行时就调用lock.wait()阻塞住,在thread2中等待3s之后唤醒thread1,此时thread1会重新去获取锁并继续向下执行。
我们将例子改造一下来证明线程被wait之后会释放掉锁持有的同步锁。
public Object lock = new Object();
public void main() throws InterruptedException {
Thread thread1 = new Thread(() -> {
System.out.println("执行" + Thread.currentThread().getName() + "线程");
synchronized (lock) {
try {
System.out.println("将" + Thread.currentThread().getName() + "阻塞在lock对象上");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "线程执行完了");
}, "thread1");
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
synchronized (lock) {
try {
System.out.println(Thread.currentThread().getName() + "线程获得了锁,并执行同步代码");
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "thread2");
thread1.start();
//将thread2线程启动向后延迟1s以确保thread1必定能先执行到
Thread.sleep(1000L);
thread2.start();
}
输出结果:
执行thread1线程
将thread1阻塞在lock对象上
thread2线程获得了锁,并执行同步代码
thread2线程获得了锁,并执行同步代码
thread2线程获得了锁,并执行同步代码
thread2线程获得了锁,并执行同步代码
thread2线程获得了锁,并执行同步代码
以上结果说明当一个线程被wait之后,是会释放掉该线程持有的锁的。假如锁没有被释放的话,那thread2的打印代码一句都执行不到。
最后还有个notifyAll()方法,其实和notify()方法的作用一模一样,唯一的不同是调用lock.notifyAll()之后,会唤醒lock对象的等待集合中的所有线程,但是具体哪个线程先得到执行权并获得锁执行代码是不一定的,这和cpu调度线程有关。
并且我们注意一下
1.不管是lock.wait()方法包在多少层的synchronized(lock)代码块中,执行的结果也是一样的,反正也是得释放锁。
2.不管他外层包了多少层的synchronized(lock),synchronized(lock1),它都是调用了哪个lock对象的wait()方法,线程就会等在该lock对象的等待集合中,其他的锁是不会释放的。
说到2的时候可能比较抽象,我们再将代码改造一下看看效果:
public Object lock = new Object();
public Object lock1 = new Object();
public void main() throws InterruptedException {
Thread thread1 = new Thread(() -> {
System.out.println("执行" + Thread.currentThread().getName() + "线程");
synchronized (lock) {
synchronized (lock1) {
try {
System.out.println("将" + Thread.currentThread().getName() + "阻塞在lock对象上");
lock1.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println(Thread.currentThread().getName() + "线程执行完了");
}, "thread1");
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
synchronized (lock) {
try {
System.out.println(Thread.currentThread().getName() + "线程获得了锁,并执行同步代码");
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "thread2");
thread1.start();
//将thread2线程启动向后延迟1s以确保thread1必定能先执行到
Thread.sleep(1000L);
thread2.start();
}
输出结果:
执行thread1线程
将thread1阻塞在lock对象上
此时我们在thread1中先获取了一个同步锁lock,紧接着在里面再获取一个lock1同步锁,这时候在代码中调用lock1.wait()将线层阻塞,此时该线程会等待在lock1的等待集合中并且释放锁lock1,但是thread2要执行的时候需要获取lock的锁,这时候lock的锁还被thread1占着,也就是说lock对象里的monitor中的_Owner的线程对象还是thread1,所以thread2永远得不到执行。
希望本文章能对Thread中wait和notify这个线程的交互机制还不熟悉的朋友有点参考意义。