线程基本通信机制

一、线程基本通信机制

1 wait和notify的用法

wait和notify是Java最基本的线程间通信机制,体现了线程的交互,用于信息的传递。例如在生产者消费者模式中,利用阻塞和唤醒使不同线程之间配合实现业务逻辑。

阻塞阶段--wait,调用对象的wait方法,线程进入WAITING状态,阻塞挂起,释放锁
wait阻塞后,直到下面情况之一发生时,线程才会被唤醒。

  • 其他线程调用该对象的notify/notifyAll方法。
  • 带有超时参数的wait方法,发生超时。如果参数是0,则永久等待。
  • 调用线程中断interrupt()。线程在waiting状态,会自动响应中断,抛出中断异常。

唤醒阶段 --notify/notifyAll

  • notify:随机唤醒一个线程
  • notifyAll:唤醒所有线程

2 wait和notify性质

  1. 使用前需要拥有锁(monitor锁)
  2. 属于Object类,底层是final native方法,属于JVM层代码。
  3. wait和notify是最基本的线程通信机制
  4. 同时拥有多把锁,需要注意锁的释放顺序
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的初衷就是用来线程间通信,如果以线程为锁,不利于设计流程。

posted @ 2020-12-20 22:24  Awecoder  阅读(418)  评论(0编辑  收藏  举报