文章目录
- 阻塞队列理论
- 为什么用阻塞队列?有什么好处?
- 接口结构和实现类
- 阻塞队列的核心方法
- 抛出异常组:add/remove
- 返回bool值组:offer/poll
- 阻塞:put/take
- 超时控制:offer/poll
- 同步synchronousQueue队列
- 线程通信之生产者消费者传统版
- 虚假唤醒
- sync与lock的区别
- 原始构成
- 使用方法
- 等待是否可以中断
- 加锁方式
- 锁要绑定多个条件condition
阻塞队列理论
空了,消费者阻塞,满了,生产者阻塞。
MQ的核心底层原理就是这个。
为什么用阻塞队列?有什么好处?
有了它,程序员不需要再去控制wait和notify。这是它的厉害之处。
接口结构和实现类
Collection集合接口有2个子接口,List和阻塞队列BlockingQueue。
collection-queue接口-阻塞队列接口。
阻塞队列接口有7个实现类。
链表结构的有界阻塞队列绝对不允许用size默认值(21亿)。可以当作无界了。
标红的三个就是线程池底层用的三个。
其中synchronousQueue:理解为订单不下,我不开工!
queue接口跟list一样,arraylist,linkedlist对应的arrayblockingqueue,linkedbolockingqueue。
阻塞队列的核心方法
抛出异常组:add/remove
add方法报错演示:
add方法异常触发代码:
取出元素抛出异常代码演示:
错误信息展示:
element方法:查看队列排头(先进先出)的一个元素,并不取出,如果没有元素也会抛出异常。
返回bool值组:offer/poll
这组api不会抛出异常。是返回的指定的值。
api测试代码:
api测试结果:
阻塞:put/take
这一组慎用,但是用好了对程序帮助很大。
测试代码:
运行结果:注意此时jvm没有停止,在一直尝试添加。消息积压之后,只能堵着。
插入方法和取出方法一旦执行的不顺利(队列满了加不进去,队列空了取不出来)则会堵塞在这。
超时控制:offer/poll
结果是只阻塞2s,然后返回false。
同步synchronousQueue队列
典型的先下单,再生产的情形。
空构造是一个非公平锁。并不存储。
代码演示:
运行结果:
一定是先等消费者来了,才往里面放,否则就卡在那里。
线程通信之生产者消费者传统版
阻塞队列用在哪里呢?请看上图。
线程通信模型的迭代:从左到右,是1.0到2.0.
多线程口诀:高内聚,内耦合,线程操作资源类。判断-干活-唤醒通知。严防多线程状态下的虚假唤醒(多线程判断用while)。
所有的java操作,先是对象,再是对象里的变量。
代码演示:
线程调用资源类的方法,而不是线程里再去写方法,是资源类自带的对外访问的接口。
测试代码:
运行结果:生产一个,消费一个。
这就是传统版本的生产者消费者模式,就是上面那两个三角形的变动。
虚假唤醒
对于object类的wait和notify方法,api提示了可能发生虚假唤醒。所以方法要放在循环里面。
所以多线程的判断,你不能用if,要用while。也就是说被唤醒之后,要重新做一次判断,是否真的该轮到我执行了。
想复现虚假唤醒,可以把测试线程翻倍,然后看测试结果。如果不是while判断的话,改为if,就会出现虚假唤醒了。测试结果如下图:
只要是多线程框架的底层,基本都是while判断。比如netty。
两个线程,互相唤醒,没问题,如果线程多了,唤醒了同为生产者/消费者的线程,抢到了锁,就会出问题,也就是所谓的虚假唤醒了。
sync与lock的区别
某学堂-JUC-2019版本讲过。
之前的笔记:
原始构成
看一下jvm的字节码,sync关键字是依靠monitor系列的关键字实现的。
sync底层会有2次退出,第一次保证正常退出,第二次保证异常退出。
sync保证你不会产生死锁和底层的一些故障。因为有这两个退出。
而关于new ReentrantLock,底层代码则为:
是API级别的锁。
使用方法
sync不需要手动释放锁,但是Reentrant需要手写lock和unlock。
等待是否可以中断
加锁方式
锁要绑定多个条件condition
题目要求:
代码设计:
修改标志位,然后通知:
print15:
最后资源类效果:
测试代码:
运行结果会按照5-10-15的次数进行打印。