阻塞队列和线程池原理

504 阅读5分钟

一、阻塞队列。

1.BlockingQueue

非阻塞方法:

  • add()、remove() 会有异常(当满队列里加,空队列里取)
  • offer()、poll() 满队列加返回false.空队列取会返回null。

阻塞方法:

-take()、put() 满队列加、空队列取都会阻塞。

2. 常用的阻塞队列

  • ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
  • LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
  • PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
  • DelayQueue:一个使用优先级队列实现的无界阻塞队列。
  • SynchronousQueue:一个不存储元素的阻塞队列。
  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
  • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

有界:规定了最大容量。

无界:没有上线。

以上的阻塞队列都实现了BlockingQueue接口,也都是线程安全的。

二、线程池

1.参数介绍

public ThreadPoolExecutor(int corePoolSize,//核心线程数量
                          int maximumPoolSize,//最大线程数量
                          long keepAliveTime,//空闲线程存活时间。
                          TimeUnit unit,//存活时间的单位
                          BlockingQueue<Runnable> workQueue,//存储的阻塞队列
                          ThreadFactory threadFactory,//创建线程用什么方式创建
                          RejectedExecutionHandler handler)//拒绝策略

corePoolSize

线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;

如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;

如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。

maximumPoolSize

线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize

keepAliveTime

线程空闲时的存活时间,即当线程没有任务执行时,继续存活的时间。默认情况下,该参数只在线程数大于corePoolSize时才有用

TimeUnit

keepAliveTime的时间单位

workQueue

workQueue必须是BlockingQueue阻塞队列。当线程池中的线程数超过它的corePoolSize的时候,线程会进入阻塞队列进行阻塞等待。通过workQueue,线程池实现了阻塞功能。

一般来说,我们应该尽量使用有界队列,因为使用无界队列作为工作队列会对线程池带来如下影响。

1)当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中的线程数不会超过corePoolSize。

2)由于1,使用无界队列时maximumPoolSize将是一个无效参数。

3)由于1和2,使用无界队列时keepAliveTime将是一个无效参数。

4)更重要的,使用无界queue可能会耗尽系统资源,有界队列则有助于防止资源耗尽,同时即使使用有界队列,也要尽量控制队列的大小在一个合适的范围。

threadFactory

创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名,当然还可以更加自由的对线程做更多的设置,比如设置所有的线程为守护线程。

Executors静态工厂里默认的threadFactory,线程的命名规则是“pool-数字-thread-数字”。

RejectedExecutionHandler

线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:

(1)AbortPolicy:直接抛出异常,默认策略;

(2)CallerRunsPolicy:用调用者所在的线程来执行任务;

(3)DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;

(4)DiscardPolicy:直接丢弃任务;

当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。

2.线程池的运行流程。

1.线程池创建出来,会根据corePool数量创建相应数量的线程等待任务的执行。
2.当任务数超过corePool的数量的时候,会把任务放在BlockingQueue里.
3.当BlockingQueue里任务满了。会根据MaxmumPool的数量取创建线程来处理任务。
4.当线程数量达到MaxmumPool,任务依然还在增加,则执行拒绝策略。

image.png

3.提交任务

execute()方法

用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。

submit()方法

用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

4.关闭线程

可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程

shutdown:标记线程池为shutdown状态,然后关闭所有没有执行任务的线程。

shutdownnow:标记线程池为stop状态,对所有执行的线程调用interrupt方法。(可以有线程不关闭,直到任务执行完。)

5.线程池的合理配置

最大线程池设置根据任务的特性:

  • cpu密集型 :最大线程池数量不要超过cpu的核心数(最多+1,在缺页中断的时候把cpu利用上)。Runtime.getRuntime().availableProcessors()
  • IO密集型 :最大线程池数量 = cpu核心数*2
  • 混合型 :平均拆分,不平均往上面俩去靠。

BlockingQueue的设置

最好是有界的阻塞队列。否则最大线程数和时间都没作用了。并造成OOM。