一、线程基本通信机制
1 wait和notify的用法
wait和notify是Java最基本的线程间通信机制,体现了线程的交互,用于信息的传递。例如在生产者消费者模式中,利用阻塞和唤醒使不同线程之间配合实现业务逻辑。
阻塞阶段--wait,调用对象的wait方法,线程进入WAITING状态,阻塞挂起,释放锁。
wait阻塞后,直到下面情况之一发生时,线程才会被唤醒。
- 其他线程调用该对象的notify/notifyAll方法。
- 带有超时参数的wait方法,发生超时。如果参数是0,则永久等待。
- 调用线程中断interrupt()。线程在waiting状态,会自动响应中断,抛出中断异常。
唤醒阶段 --notify/notifyAll
- notify:随机唤醒一个线程
- notifyAll:唤醒所有线程
2 wait和notify性质
- 使用前需要拥有锁(monitor锁)
- 属于Object类,底层是final native方法,属于JVM层代码。
- wait和notify是最基本的线程通信机制
- 同时拥有多把锁,需要注意锁的释放顺序
synchronized(this) {
while(条件){
wait();
}
}
- 如果wait方法没有修饰,表示当前对象this
- 即使没有调用唤醒方法,线程仍有可能从挂起状态变为可运行状态(虚假唤醒)。为防患于未然,常见是不断测试该线程被唤醒的条件是否被满足,不满足则继续等待。
synchronized (resourceA) {
synchronized (resourceB) {
try {
resourceA.wait(); // 只释放resourceA锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
注意点:调用某个对象的notify,只能唤醒与该对象对应的线程。调用wait也只释放当前的那把锁。
3 wait原理
EntrySet:入口集;WaitSet:等待集;The owner:线程拥有锁。’
锁的运行原理:开始线程在入口集和等待集竞争锁【1】,此时线程A获取到了锁【2】,入口集和等待集中的线程进入BLOCKED。此时A可以正常运行完释放锁【6】,也可以调用wait释放锁进入等待集【3】。等待集线程被唤醒【4】后,进入另一个等待集,与入口集的线程一起竞争锁【5】。
二、常见问题
1 wait和notify实现生产者消费者模式
生产者消费者模式可以解耦生产者和消费者,使两者更好地配合。
// 生产和消费100个产品
public class ProducerConsumerModelByWaitAndNotify {
public static void main(String[] args) {
// 创建仓库
Storage storage = new Storage();
// 创建生产者消费者线程
Thread producer = new Thread(new ProducerTask(storage));
Thread consumer = new Thread(new ConsumerTask(storage));
producer.start();
consumer.start();
}
}
class ProducerTask implements Runnable {
private Storage storage;
public ProducerTask(Storage storage) {
this.storage = storage;
}
@Override
public void run() {
// 生产100个产品
for (int i = 0; i < 100; i++) {
storage.put();
}
}
}
class ConsumerTask implements Runnable {
private Storage storage;
public ConsumerTask(Storage storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
storage.take();
}
}
}
class Storage {
private int maxSize;
private Queue<Date> storage;
public Storage() {
this.maxSize = 10; // 队列最大是10
this.storage = new LinkedList<>();
}
/**
* wait和notify需要首先获取到锁,因此需要使用synchronized方法或者同步代码块
*/
public synchronized void put() {
// 仓库已满,无法生产更多产品,让出锁
while (storage.size() == maxSize) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
storage.add(new Date());
System.out.println("生产者生产产品,此时仓库产品数:" + storage.size());
// 通知消费者消费
notify();
}
public synchronized void take() {
// 仓库为空,无法获取到产品,线程让出锁
while (storage.size() == 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
storage.poll();
System.out.println("消费者消费产品,此时仓库产品数:" + storage.size());
// 通知生产者生产
notify();
}
}
2. 为什么wait需要放在同步代码块中使用,而sleep不需要?
主要是为了让通信更加可靠,防止死锁、永久等待的发生。
wait放到synchronized代码中对线程有一定的保护作用。假设没有synchronized的保护,线程A在运行到wait语句之前,切换到线程B执行了notify语句,此时执行了wait语句释放锁后,没有线程唤醒,导致了永久等待。
sleep方法是针对单个线程的,与其他线程无关,无需放入到同步代码块中。
3 为什么wait和notify方法定义在Object中,而不是Thread中?
wait和notify是锁级别操作,而锁是属于某个对象的,锁标识在对象的对象头中。如果将wait和notify定义在线程中,则会有很大的局限性。例如每个线程都可能会休眠。如果某个线程持有多个锁,而且锁之间是相互配合的时,wait方法在Thread类中,就没有办法实现线程的配合。
调用线程对象.wait会发生什么?
个人理解:
调用线程对象的wait方法,也就是说以线程为锁。wait和notify的初衷就是用来线程间通信,如果以线程为锁,不利于设计流程。