阻塞队列BlockingQueue

    在集合的顶级父类Collection下,除了有List和Set接口外,还有一个与它们同级的接口Queue,Queue的子类BlockingQueue就是阻塞队列。

阻塞队列BlockingQueue,线程池的正确用法_抛出异常

diagrams图

怎么理解阻塞队列?

1. 写入:如果队列满了,就必须阻塞等待

2. 取:如果队列为空,必须阻塞等待生产

阻塞队列BlockingQueue,线程池的正确用法_线程池_02

阻塞队列四组API1、有返回值,会抛出异常

阻塞队列BlockingQueue,线程池的正确用法_阻塞队列_03

如果队列满了继续添加,会抛异常:

java.lang.IllegalStateException: Queue full

如果列队为空继续取出,会抛异常:

java.util.NoSuchElementException2、有返回值,不抛异常

阻塞队列BlockingQueue,线程池的正确用法_抛出异常_04

如果队列满了继续添加,返回false;

如果队列为空继续取出,返回null。3、等待,阻塞(一直阻塞)

阻塞队列BlockingQueue,线程池的正确用法_阻塞队列_05

如果队列满了继续添加,会一直阻塞等待;

如果队列为空继续取出,也会一直阻塞等待。4、等待,阻塞(等待超时)

阻塞队列BlockingQueue,线程池的正确用法_线程池_06

    使用offer()不仅可以向队列中添加元素,还可以指定超时时间。当队列满了之后,不会立即返回,而是阻塞等待指定时间后再返回;

    同理使用poll()不仅可以取出元素,也可以指定超时时间。当队列为空时,阻塞等待指定时间后再返回。


同步队列SynchronousQueue

    SynchronousQueue是BlockingQueue的实现类,它是一个特殊的阻塞队列。进去一个元素,必须等待取出来之后,才能再往里面放一个元素。一句话描述就是容量为1的阻塞队列,特别简单。


代码验证:开启两个线程T1、T2,T1存值,T2每次等待3秒取值

阻塞队列BlockingQueue,线程池的正确用法_线程池_07

执行结果:

阻塞队列BlockingQueue,线程池的正确用法_线程池_08

阻塞队列BlockingQueue,线程池的正确用法_线程池_09【结果分析】

    从结果可以清晰的看到,T1线程put完元素1之后,并没有马上put元素2,而是等线程2取出之后,才put进去。


线程池

为什么使用线程池?

    使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

在阿里巴巴开发手册上关于线程池有这么一段描述:

阻塞队列BlockingQueue,线程池的正确用法_阻塞队列_10

    如果对线程池了解不深的话,这段话并不容易理解。而且直到目前为止,我还经常见到同事使用Executors去创建线程,这是很危险的方式。学习线程池的秘诀:

三大方法、7大参数、4种拒绝策略

创建线程池的三大方法:

阻塞队列BlockingQueue,线程池的正确用法_抛出异常_11

阻塞队列BlockingQueue,线程池的正确用法_线程池_09​【源码分析】

分别点进去三个方法,查看对应的源码:

阻塞队列BlockingQueue,线程池的正确用法_线程池_13

阻塞队列BlockingQueue,线程池的正确用法_抛出异常_14

阻塞队列BlockingQueue,线程池的正确用法_抛出异常_15

    不难发现,三大方法其实都是调用的ThreadPoolExecutor去开启线程池,那这是个什么玩意儿?

阻塞队列BlockingQueue,线程池的正确用法_阻塞队列_16

重点:七大参数!


  • corePoolSize 核心线程池大小

  • maximumPoolSize 最大核心线程池大小

  • keepAliveTime 超时了,没有人调用就会释放

  • unit 存活时间的单位

  • workQueue 阻塞队列

  • threadFactory 线程工厂:创建线程的

  • handler 拒绝策略


^_^ 举一个银行办理业务的例子来深刻理解这7大参数:

    假设银行有5个窗口,但平时不会全部开放,如果只有2个人来办理业务,完全能满足;如果这时候来了3个人,也不会开放窗口,而是进入候客区等待(假设候客区有3个位置);如果窗口和候客区全部满了,即来了第6个人,则会增加开放窗口;如果所有窗口都开放且候客区也满了,又来了一个人,那么银行就不让进了。

阻塞队列BlockingQueue,线程池的正确用法_抛出异常_17

那么将这个场景对应到线程池中:

柜台平时开放的窗口就是核心线程池的大小;

柜台所有窗口就是最大线程池的大小;

候客区就是阻塞队列;

在所有窗口和候客区满后,拒绝客户进入就是拒绝策略。

阻塞队列BlockingQueue,线程池的正确用法_抛出异常_18

再将上面的场景对应到代码中:

阻塞队列BlockingQueue,线程池的正确用法_线程池_19

阻塞队列BlockingQueue,线程池的正确用法_线程池_09【场景分析】

我们可以手动修改 i 的最大值来模拟办理业务的人数:

1. 当 i 为 2 时,有2个线程在运行

阻塞队列BlockingQueue,线程池的正确用法_抛出异常_21

2. 当 i 为 5 时,仍然是2个线程在运行

阻塞队列BlockingQueue,线程池的正确用法_线程池_22

3. 当 i 为 6 时,有3个线程在运行,说明触发了最大线程数

阻塞队列BlockingQueue,线程池的正确用法_线程池_23

4. 当 i 为 8 时,最多时有5个线程在运行

阻塞队列BlockingQueue,线程池的正确用法_抛出异常_24

5. 当 i 为 9 时,就会抛出异常

阻塞队列BlockingQueue,线程池的正确用法_阻塞队列_25

这里抛异常是因为我们使用了默认的拒绝策略AbortPolicy(),线程池提供的拒绝策略一共有四个。四大拒绝策略

  • AbortPolicy()

    默认拒绝策略,线程池满了,如果还有线程进入,则不会处理并抛出异常

  • CallerRunsPolicy()

    在任务被拒绝添加后,会用调用execute函数的上层线程去执行被拒绝的任务

  • DiscardPolicy()

    队列满了,丢掉任务,且不抛异常,啥也不干

  • DiscardOldestPolicy()

    当任务被拒绝添加时,将最先加入队列的任务丢掉,之后在尝试加入队列

Q:线程池的大小设置多少合适?

这个问题要分为两个纬度去分析:

1. CPU密集型

    几核就设置为几,并行执行,提高CPU利用率。

2. IO密集型

    判断程序中十分耗IO的线程数,那么线程池的大小至少要大于这个数量,一般设置为2倍。比如说有5个耗IO的线程,则线程池设置为10。


下一篇:JMM的8大原子操作


本文中涉及到的所有代码已上传到gitee和github,地址在公众号窗口 我的->git 查看相关内容