继上一节我们学习了synchronized四种应用形式,这次我们要对真正的问题动手了——生产着消费者问题
我们会引入wait notify机制 之后,结合synchronize,利用管程法和信号灯法(其实就是一个标志位)来给出生产着消费者问题的两种solution
wait 和 notify都是 object类的native方法(native方法即是由JVM的C代码实现的),而正因为他是native方法,它也是final的, 即不可被override,否则会衍生出很多问题。
wait()的作用是使当前执行wait()方法的线程等待阻塞,进入等待队列,表现出来的效果就是,在wait()所在的代码行处暂停执行,并立即释放锁,直到接到notify通知或被interrupt中断(像我们之前说的,在阻塞状态的线程停止可以用interrupt中断来实现)。
notify()的作用是通知那些可能等待该锁的其他线程,如果有多个线程等待,则按照执行wait方法的顺序发出一次性通知(一次只能通知一个!),因此,自然是在等待队列中排第一的的线程获得锁,因为他被最先释放了,其他等待的线程只能在队列待着。因为notify只是通知,因此自然与当前线程释放锁与否毫无关系:)
notify方法只唤醒一个处于等待阻塞状态的线程并使该线程开始执行。所以如果有多个线程等待一个资源,这个方法只会唤醒队列中的第一个线程,因为队列是先入先出,因此就是最早被等待阻塞的线程
而notifyAll 会唤醒所有等待的线程,因此常用的就是notifyAll 方法。
比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行。
上面讲到wait的时候提到,他会使得线程阻塞,
奇怪了 还记得上一节我们提到的阻塞,似乎都与 “占着茅坑不拉屎”,被阻塞了还占用着资源,这些说法相关啊,这里的wait阻塞为啥反而是释放锁呢???
我们想想阻塞的定义:
阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行,经过一系列操作,直到线程进入就绪状态Runnable,才有机会转到运行状态Running。
阻塞更多的是种表象,你只能说CPU暂时不运行它了 而且他也不是在就绪状态,至于他是占用着锁,还是释放了锁,其实看情况的
具体来说,阻塞有三种:
等待阻塞 运行的线程执行wait()方法,JVM会把该线程放入等待队列中,同时值得注意的是,wait会释放持有的锁
同步阻塞 运行的线程在获取对象的同步锁时,也即是为了访问对象资源的时候,若该同步锁被别的线程抢先占用,则该线程被阻塞,JVM会把该线程放入锁池中。
其他阻塞 运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。值得注意的是,这样的阻塞,是不会释放持有的锁
很明显,第三种是很常见的,也是导致之前讨论过的诸多问题的阻塞,“经典的”阻塞不会释放锁。
我们通过状态图,来看看线程的状态变化是怎么样的:
我们的三种阻塞,其实分别走向了三个路径,
或许这些名词不够舒服 我们看另一张图:
这里说得很清楚,阻塞状态广义上包括:
另外,说明一下,之前之所以没有放这张图,是因为我们好多东西没学,放了看不懂,只会徒增烦恼额,而现在,我们学习了join和yield,也稍微了解了wait和notify 这个图什么意思我们心里都有数了。如果还不太能接受,可以看完后边的生产者消费者问题再回头看看。
听起来好像很玄乎的样子 我们假设有三个角色,生产产品的人,消费产品的人,放产品的仓库,
生产者,把产品放到仓库里,并且仓库空间有限,所以满了就不放了呗,
消费者,从仓库中拿产品,并且产品有限,拿完了就没了,因此拿完了消费者也没法做事了
由于多线程,我们这个仓库要作为临界资源,即同时只能有一个线程访问之,否则会造成数据不安全。目前我们所知的就是使用Synchronized代码块来实现,所谓的同步访问
换言之,仓库满了,生产者线程应当阻塞,而且是等待阻塞,why?因为仓库满了 你生产者不应该占用仓库而不干事啊:)这时应当让消费者线程进去,消费产品。
但是这里有个问题了 消费者怎么知道 你啥时候仓库满了?所以这就需要所谓的线程通讯机制,我们之前学Synchronized是怎么保证线程安全,也就是实现同步访问,同时只能有一个线程能够修改临界资源,这里则需要的是线程之间的通讯与协调。
聪明的你应该能想到,为啥我们先介绍了奇怪的wait和notify -> 没错,wait / notify 正是一种线程通讯机制。
前面我们说了,wait 方法使线程暂停运行,等待阻塞,而notify 方法通知所有处于等待队列的线程继续运行。
但是要想正确使用wait/notify,一定要注意四点:
wait/notify 调用,其object必须是临界资源,是有锁的,否则程序会抛出异常,也就调用不了wait/notify;
Why?其实答案很明显,如果,这个object压根就不是临界资源,那我等待阻塞什么?阻塞线程其实没有必要了,同样,如果这个object不是临界资源,那也没有所谓等待的队列,没有线程会因为非临界资源而被等待阻塞。
所以另外一个注意点也很简单,wait和notify必须配合Synchronized 或者其他能够使得object称为临界资源,能够给他上锁的机制来使用 这篇文章来说我们就是把wait和notify放在Synchronized代码块中的。
还有个注意点,如果wait和notify所服务的不是同一个object,即不是同一把锁,那也不起作用,wait阻塞的线程,进入队列之后,应当有与之对应的notify来通知唤醒!
最后,这点可能需要看下后边代码才能理解,就是在编程中,尽量在使用了notify/notifyAll() 后立即退出临界区,这样唤醒其他线程后,被唤醒的线程们可以立即获得锁。
为啥?比如正常来说,你用工具干完了,叫下一个人接着干,可如果你干到一半,活还没干完,你就唤醒人家,但是你自己又占着锁,这不是逗人家玩嘛(人家还是只能干等着,等待阻塞状态)。。
不知道这个名字啥意思,反正对于wait / notify 通讯机制,经典的一种应用方式就是管程法。思路也很简单,
首先生产者和消费者之间,要设立缓冲区,
生产者把产品放进去,检测到缓冲区的产品满了,就阻塞自己,
同样的,如果消费者消费的时候发现产品没了,也会阻塞自己确保库存不是负数
当然了 对生产者而言,要是产品没有满,应该怎么办呢?答案是唤醒别的线程(包括消费者和生产者)来消费
而反之消费者发现产品还没有空 他也会唤醒别的线程来生产。
仓库Storage类的代码如下:
import java.util.LinkedList; public class Storage implements AbstractStorage { //仓库最大容量 private final int MAX_SIZE = 100; //仓库存储的载体 private LinkedList list = new LinkedList(); //生产产品 public void produce(int num){ //同步 synchronized (list){ //仓库剩余的容量不足以存放即将要生产的数量,暂停生产 while(list.size()+num > MAX_SIZE){ System.out.println("【要生产的产品数量】:" + num + "\t【库存量】:" + list.size() + "\t暂时不能执行生产任务!"); try { //条件不满足,生产阻塞 list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } for(int i=0;i<num;i++){ list.add(new Object()); } System.out.println("【已经生产产品数】:" + num + "\t【现仓储量为】:" + list.size()); list.notifyAll(); } } //消费产品 public void consume(int num){ synchronized (list){ //不满足消费条件 while(num > list.size()){ System.out.println("【要消费的产品数量】:" + num + "\t【库存量】:" + list.size() + "\t暂时不能执行生产任务!"); try { list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //消费条件满足,开始消费 for(int i=0;i<num;i++){ list.remove(); } System.out.println("【已经消费产品数】:" + num + "\t【现仓储量为】:" + list.size()); list.notifyAll(); } } }
其抽象接口的代码如下:
public interface AbstractStorage { void consume(int num); void produce(int num); }
对于仓库而言,他需要实现consume(消费)和生产(product)两个方法,Why?
因为仓库和产品绑定在一起,对于这种产品,我们只能这么生产和消费。如果让生产者来实现生产方法,消费者实现消费方法,假设还有别的仓库和产品,该怎么办呢?我们即便采用 根据不同输入参数的方式来分流不同的生产方式,也不能很好地解决问题,因为代码臃肿不堪,与仓库的耦合度太高,另外也不符合开闭原则。
或许可以尝试工厂模式?不过遗憾的是本节重点不在这里,可能我们聊到设计模式的时候会改进目前的代码:)就目前而言我觉得仓库和生产消费方法同属于一个类,然后通过组合composite的方式来给线程类使用 是一个不错的方式。
另外 为啥使用LinkedList?
因为这里增删的产品完全相同,因此只需要链表末尾元素增删,不需要随机访问的功能,那自然排除增删性能没那么好的ArrayList。
为啥需要一个抽象的接口AbstractStorage?
我说了,假设还有别的仓库,别的产品,那我们该怎么组合仓库到我们的消费者生产者里边呢?(组合是啥意思 不明确的话请先移步下边生产者的代码)既然具体类我组合不了,我再抽象一层他不香吗:)如果愿意,你甚至可以运用工厂模式,抽象工厂模式,给你弄各种各样的产品哈哈。
public class Producer implements Runnable{ //所属的仓库 public AbstractStorage abstractStorage; public Producer(AbstractStorage abstractStorage){ this.abstractStorage = abstractStorage; } // 线程run函数 public void run() { Random random = new Random(); abstractStorage.produce(random.nextInt(50)); } }
public class Consumer implements Runnable { // 所在放置的仓库 private AbstractStorage abstractStorage; // 构造函数,设置仓库 public Consumer(AbstractStorage abstractStorage1) { this.abstractStorage = abstractStorage1; } // 线程run函数 public void run() { Random random = new Random(); abstractStorage.consume(random.nextInt(50)); } }
public class Test { public static void main(String[] args) { // 仓库 Storage storageHuaWei = new Storage("HuaWei",100); Storage storageIphone = new Storage("iphone",100); Producer HuaWeiProducer = new Producer(storageHuaWei); Consumer HubWeiConsumer = new Consumer(storageHuaWei); // 造十个华为生产者 for (int i = 0; i < 10; i++) new Thread(HuaWeiProducer, "HuaWeiProducer_"+i).start(); // 造十个华为消费者 for (int i = 0; i < 10; i++) new Thread(HubWeiConsumer, "HuaWeiProducer_"+i).start(); } }
这里体现出来Runnable的优势,避免了线程类的泛滥(可以看看此专栏第一篇第二篇介绍的 Thread和Runnable两种方法的区别)
另外,使用了抽象Storage接口,使得我们可以更灵活的生产与消费,比如建立iphone的仓库,创建iphone的生产者和消费者。这里还可以进一步运用工厂模式来拓展功能,读者们可以想想该怎么操作。
已生产HuaWei产品数:23 【现仓储量为】:23 预出货HuaWei产品数量:32 【库存量】:23 出货任务等待阻塞 已出货HuaWei产品数量:14 【现仓储量为】:9 预出货HuaWei产品数量:38 【库存量】:9 出货任务等待阻塞 预出货HuaWei产品数量:23 【库存量】:9 出货任务等待阻塞 预出货HuaWei产品数量:14 【库存量】:9 出货任务等待阻塞 预出货HuaWei产品数量:11 【库存量】:9 出货任务等待阻塞 预出货HuaWei产品数量:29 【库存量】:9 出货任务等待阻塞 已出货HuaWei产品数量:1 【现仓储量为】:8 已出货HuaWei产品数量:5 【现仓储量为】:3 预出货HuaWei产品数量:27 【库存量】:3 出货任务等待阻塞 已生产HuaWei产品数:40 【现仓储量为】:43 已生产HuaWei产品数:14 【现仓储量为】:57 已生产HuaWei产品数:10 【现仓储量为】:67 已生产HuaWei产品数:22 【现仓储量为】:89 预生产HuaWei产品数量:12 【库存量】:89 生产任务等待阻塞 预生产HuaWei产品数量:39 【库存量】:89 生产任务等待阻塞 已生产HuaWei产品数:7 【现仓储量为】:96 预生产HuaWei产品数量:29 【库存量】:96 生产任务等待阻塞 预生产HuaWei产品数量:26 【库存量】:96 生产任务等待阻塞 预生产HuaWei产品数量:39 【库存量】:96 生产任务等待阻塞 预生产HuaWei产品数量:12 【库存量】:96 生产任务等待阻塞 已出货HuaWei产品数量:27 【现仓储量为】:69 已出货HuaWei产品数量:29 【现仓储量为】:40 已出货HuaWei产品数量:11 【现仓储量为】:29 已出货HuaWei产品数量:14 【现仓储量为】:15 预出货HuaWei产品数量:23 【库存量】:15 出货任务等待阻塞 预出货HuaWei产品数量:38 【库存量】:15 出货任务等待阻塞 预出货HuaWei产品数量:32 【库存量】:15 出货任务等待阻塞 已生产HuaWei产品数:12 【现仓储量为】:27 已生产HuaWei产品数:39 【现仓储量为】:66 已生产HuaWei产品数:26 【现仓储量为】:92 预生产HuaWei产品数量:29 【库存量】:92 生产任务等待阻塞 已出货HuaWei产品数量:32 【现仓储量为】:60 已出货HuaWei产品数量:38 【现仓储量为】:22 预出货HuaWei产品数量:23 【库存量】:22 出货任务等待阻塞 已生产HuaWei产品数:29 【现仓储量为】:51 已出货HuaWei产品数量:23 【现仓储量为】:28 Process finished with exit code 0
在多线程中要测试某个条件的变化,使用if 还是while?
给个结论 用while,
注意,notify唤醒沉睡的线程后,线程会接着上次的执行继续往下执行。所以,之前,因为不符合条件,wait执行导致自己被阻塞,但是当被唤醒的时候,程序接着往下跑,(还记得吗,我们有线程控制块Thread Control BLock能够很好地保存现场,程序计数器PC能够精确到上次运行到的代码处)
如果你是If 往下跑就跑没了 但事实上是我们想要的吗?比方说作为消费者,if里边检查的是还有没有库存,如果是if,被唤醒以后直接跑下去了,但是唤醒了不代表此时库存满足条件,这样很可能导致负数——因为你一唤醒就执行,也不管条件是否真的符合
显然,需要使用while,是得条件满足才能继续。
其实换汤不换药,之前我们通过判断一些条件来决定是否需要wait,比如库存没了需要生产者,或者库存满了需要消费者,换言之,你可以把这句话num > list.size()
就当做是所谓的信号灯flag就完事了
就只不过是通过判断flag来指示是否wait,与原来的条件判断一样,你只需要保证flag得到线程安全的维护即可 这里略过
这一节我们学习了wait /notify 机制,即一部分线程通讯的知识,用以解决经典的生产者消费者问题,当然wait和notify有自己的固有缺陷,哪还有没有更好的方式呢?另外,Synchronized方法,又没有更好的替代品呢?他们的缺陷是什么?这些会在随后的文章中介绍。
下一节 Java 从多线程到并发编程(八)——
正在更新