notify,notifyAll区别(生产者消费者案例)

1.代码示例

1.1 生产者

package com.example.hxk.thread.demo;

import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author Smith 2019/3/21
 */
public class Producer implements Runnable{

    List<Integer> cache;

    public void put() throws InterruptedException {
        synchronized (cache) {
            while (cache.size() == 1) {
                try {
                    cache.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }

            TimeUnit.SECONDS.sleep(1);

            cache.add(1);
            System.out.println(Thread.currentThread().getName() + "生产者生产了一条。");
            cache.notify();
        }
    }

    public Producer(List<Integer> cache) {
        this.cache = cache;
    }

    @Override
    public void run() {
        while (true) {
            try {
                put();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

1.2 消费者

package com.example.hxk.thread.demo;

import java.util.List;

/**
 * @author Smith 2019/3/21
 */
public class Customer implements Runnable {

    List<Integer> cache;

    public Customer(List<Integer> cache) {
        this.cache = cache;
    }

    private void custom() {
        synchronized (cache) {
            while (cache.size() == 0) {
                try {
                    cache.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            cache.remove(0);
            System.out.println(Thread.currentThread().getName() + "消费者消费了一条。");
            cache.notify();
        }
    }
    @Override
    public void run() {
        while (true) {
            custom();
        }
    }
}

1.3 测试代码

1.3.1 一个生产者一个消费者

package com.example.hxk.thread.demo;

import java.util.ArrayList;
import java.util.List;

/**
 * @author Smith 2019/3/21
 */
public class Test {

    public static void main(String[] args) {

        List<Integer> cache = new ArrayList<>();
        new Thread(new Producer(cache), "P1").start();
        new Thread(new Customer(cache), "C1").start();
    }
}

运行结果:

 

 

 

结论:

使用notify且一个生产者一个消费者的情况下,生产和消费有条不紊的运行,没有任何问题。

1.3.2 一个生产者两个消费者

package com.example.hxk.thread.demo;

import java.util.ArrayList;
import java.util.List;

/**
 * @author Smith 2019/3/21
 */
public class Test {

    public static void main(String[] args) {

        List<Integer> cache = new ArrayList<>();

        new Thread(new Producer(cache), "P1").start();
        new Thread(new Customer(cache), "C1").start();
        new Thread(new Customer(cache), "C2").start();
    }
}

复制代码

运行结果:

 

 

 

结论:

使用notify且一个生产者两个消费者的情况下,生产了两次后,程序死锁了。

1.3.3 将Producer和Customer中的notify()换成notifyAll()

代码就不粘了。

运行结果

 

 

 

程序又恢复了正常。

2.notify()和notifyAll的区别

每个同步对象都有自己的锁池和等待池。

2.1 锁池和等待池

  • 锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
  • 等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池.

2.2 notify()和notifyAll()的区别

  • 线程调用了wait()方法,便会释放锁,并进入等待池中,不会参与锁的竞争
  • 调用notify()后,等待池中的某个线程(只会有一个)会进入该对象的锁池中参与锁的竞争,若竞争成功,获得锁,竞争失败,继续留在锁池中等待下一次锁的竞争。
  • 调用notifyAll()后,等待池中的所有线程都会进入该对象的锁池中参与锁的竞争。

3.例子解析

知道了2的知识后,上面的三个例子也就好解释了。

1.3.1:因为只有一个生产者和消费者,所以等待池中始终只有一个线程,要么就是生产者唤醒消费者,要么消费者唤醒生产者,所以程序可以成功跑起来;

1.3.2:举个可能的例子

  1. 现在有三个线程,生产者P1, 消费者C1和C2.开始运行的时候,三个都在锁池中等待竞争,假设C1抢到锁了,C1执行时由于没有资源可以消费 调用wait()方法,释放锁并进入等待池。
  2. C2抢到了锁,开始消费,同理,C2也进入了等待池。现在锁池里面只剩下了P1.
  3. P1获得了锁,开始生产,生产完成后,P1开始调用notify()方法唤醒等待池中的C1或者C2,然后P1调用wait()方法释放锁,并进入了等待池。
  4. 假设唤醒的是C1,C1进入锁池并获得锁,消费后notify()方法唤醒了C2,C2进入锁池,C1进入等待池,现在锁池中只有C1。
  5. C1获得了锁,发现没有任何资源可以消费,wait()后释放了锁,进入了等待池,现在三个线程全都在等待池,锁池中没有任何线程。导致死锁!

1.3.3: notifyAll()后,不存在只唤醒同类线程的情况,故也就不会出现1.3.2死锁的情况。

引用

 

 

区别
notify:只会唤醒等待该锁的其中一个线程。
notifyAll:唤醒等待该锁的所有线程。
既然notify会唤醒一个线程,并获取锁,notifyAll会唤醒所有线程并根据算法选取其中一个线程获取锁,那最终结果不都是只有一个线程获取锁吗?那JDK为什么还需要做出来这两个方法呢?这两种同步方法本质上会有什么区别?

这还要从对象内部锁的调度说起。

对象内部锁
其实,每个对象都拥有两个池,分别为锁池(EntrySet)和(WaitSet)等待池。

锁池:假如已经有线程A获取到了锁,这时候又有线程B需要获取这把锁(比如需要调用synchronized修饰的方法或者需要执行synchronized修饰的代码块),由于该锁已经被占用,所以线程B只能等待这把锁,这时候线程B将会进入这把锁的锁池。
等待池:假设线程A获取到锁之后,由于一些条件的不满足(例如生产者消费者模式中生产者获取到锁,然后判断队列为满),此时需要调用对象锁的wait方法,那么线程A将放弃这把锁,并进入这把锁的等待池。
如果有其他线程调用了锁的notify方法,则会根据一定的算法从等待池中选取一个线程,将此线程放入锁池。
如果有其他线程调用了锁的notifyAll方法,则会将等待池中所有线程全部放入锁池,并争抢锁。

锁池与等待池的区别:等待池中的线程不能获取锁,而是需要被唤醒进入锁池,才有获取到锁的机会。

问题复现
那么使用notify和notifyAll到底会有什么区别呢?
请看下面一组生产者消费者的例子。
有两个生产者t1和t2,两个消费者t3和t4,以及一个长度为1的队列。

  1. 初始状态,这四个线程全部进入锁池,等待抢占锁。
  2. t3获取到锁,但是队列为空,故t3进入等待池。
  3. t4获取到锁,但是队列为空,故t4进入等待池。
  4. t1获取到锁,生产,队列满,调用notify,唤醒一个线程。由于此时t3和t4都在等待池中,所以会有一个线程从等待池进入锁池,假设此处t3进入锁池。
  5. 此时,锁池有t2和t3两个线程,假设t2获取到了锁,但是队列满,故t2进入等待池,放弃锁。
  6. 此时,t3获取到锁,消费,notify,由于此时等待池有两个线程t2和t4,假如唤醒的是t2,没问题开始生产,但是若唤醒的是t4,则因队列为空,继续wait。
  7. 此时若t1和t3已经执行结束,t1不在生产,t3不再消费,则t2和t4会一直留在等待池,行程死锁。

如果此处使用notifyAll,则会把等待池中所有线程唤醒,不会形成所有线程都位于等待池,无法唤醒的情况,也就不会形成死锁,当然了,使用notifyAll方法会更加低效一些。

如果此处是一个生产者一个消费者的情况,使用notify没有任何问题,且效率更高。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值