《Java并发编程的艺术》——线程池

为了统一管理线程和免去频繁创建销毁线程带来的不必要开销,引入了线程池,统一管理线程。

线程池分类

Java线程池基本分为5类:FixThreadPool,SingleThreadPool,CachedThreadPool,ScheduledThreadPool,ForkJoinPool,前4种通过Executors. 的方式直接创建,最后一种比较特殊,后文单独介绍。

  • FixThreadPool:固定线程数的线程池
function newFixedThreadPool(int nThreads){
 return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable\>()); 
 }
  • SingleThreadPool:只有一个线程的线程池
function newSingleThreadPool(){
 return new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable\>()); 
 }
  • CachedThreadPool:创建一个不设线程上限的线程池
function newCachedThreadPool(){
 return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable\>()); 
 }
  • ScheduledThreadPool:延迟定期执行任务的线程
function newScheduledThreadPool(){
 super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue<Runnable\>()); //这里的super就是ThreadPoolExecutor
 }

ForkJoinPool涉及到JDK 1.7之后的Fork/Join框架,这个框架的思想是把大任务分割成若干小任务,最终汇总每个小任务的结果得到大任务的结果(很像MapReduce的思想)。这个框架里应用了一种工作窃取算法,具体思想是:线程池中每个线程都有自己的工作队列,当自己的工作都完成了,就会从其他线程的工作队列中偷一个任务执行,以提高整个线程池的工作效率,窃取时则会访问到某线程的工作队列了,为了减少线程争抢,通常采用双端队列的方式实现工作队列,被窃取的线程永远从队列头拿任务执行,窃取任务的线程则从队列的末端窃取任务(注意,当队列里只剩一个任务时,不可避免的会发生线程争抢)。
这个框架的使用就是创建了ForkJoinPool对象,在它的submit方法中传入ForkJoinTask。ForkJoinTask是一个抽象类,我们只需要继承它的子类,并重写compute方法就可以使用fork和join方法了。ForkJoinTask有两个子类:RecursiveAction(用于无返回值的任务),RecursiveTask(用于有返回值的任务)。具体的任务逻辑写在compute方法里。

ThreadPoolExecutor

从上面可以看到,所有线程池的重点就是ThreadPoolExecutor,这也是核心。在阿里开发手册里说到,我们应尽量直接new ThreadPoolExecutor来创建线程池,而避免使用上述的方式创建,这样可以更直观的体现出线程池的各个参数,有利于调优。
ThreadPoolExecutor有7个参数:

  • corePoolSize:核心线程的数量,核心线程默认会一直存活,即使是没工作闲置状态
  • maximunPoolSize:允许的最大线程数(等于核心线程数+非核心线程数)
  • keepAliveTime:非核心线程的闲置存活时间,若非核心线程闲置时间超过keepAliveTime,则会被销毁(所以如果线程池没有非核心线程,这个参数并没有用),若线程池调用allowsCoreThreadTimeOut,则该参数对核心线程也有效
  • unit:上述参数的时间单位
  • workQueue:线程池使用的任务队列,目前常用的有7种
  • threadFactory:线程池中创建线程的方式
  • handler:线程无法执行新任务时,拒绝任务的策略

线程池处理新任务的流程

当向线程池提交一个新任务后,而当前线程数<corePoolSize时,会创建一个核心线程执行,若线程数已到corePoolSize,则会被放入某核心线程的工作队列中排队等待,若所有核心线程的队列已满,则会创建一个非核心线程执行,若非核心线程的数量到达maximumPoolSize,则会由配置的handler拒绝该任务。
corePoolSize -> 任务队列排队 -> 新建非核心线程执行 -> 拒绝任务

向线程池提交任务的方法

  • void execute(Runnable):不关心任务的返回结果
  • Future submit(Runnable):不关心任务的返回结果,Future的get方法会返回null
  • Future submit(Runnable, T):关心任务的返回结果,Future的get方法会返回T
  • Future submit(Callable<T>):关心任务的返回结果,Future的get方法会返回T

拒绝策略(handler)

一共有4种拒绝策略

  • CallerRunsPolicy:由提交任务者执行被拒绝的任务
  • AbortPolicy:丢弃任务并抛出异常(默认)
  • DiscardPolicy:丢弃任务不抛出异常
  • DiscardOldestPolicy:丢弃队列里最前面(最老)的任务,然后重新执行这个任务

获取任务的结果和设置超时时间

  • 对于一个任务的单个结果可使用Future.get,get方法里可设置超时时间
  • 对于多个任务的多个结果可使用ExecutorCompletionService.take,该方法总是阻塞等待一个任务完成,然后返回该任务的Future,再用get即可获得任务结果,多个任务的超时时间可借用CountDownLatch实现

WorkQueue

截止JDK 1.8,常用的工作队列有7种:

  • ArrayBlockingQueue:底层由一个数组维护,需传入一个参数作为可容纳任务数量的上界,并且可以设置的公平策略
  • LinkedBlockingQueue:底层由一个链表维护,需传入一个参数作为可容纳任务数量的上界,若不传则使用Integer.MAX_VALUE作为界
  • LinkedBlockingDeque:与LinkedBlockingQueue一样,只不过底层是双端队列
  • SynchronoueQueue:这是一个没有容量的BlockingQueue,消费者消费数据时若无数据,就会一直阻塞,直到等到一个生产者生产了数据。相当于一个消费者在等数据消费时,如果没等到,就一直等着啥也不干,直到等到数据
  • LinkedTransferQueue:可以看作是SynchronoueQueue的进化版,消费者消费数据时若无数据,就先生成一个空元素(篮子)入队(占坑),等生产者生产时发现有一个空元素,就直接把元素填入(放入篮子),相当于一个消费者在等数据消费时,如果没等到,就放个篮子,消费者也不傻等而是去干其他事,直到有生产者来了看见有篮子就直接把数据放进去,等到消费者再来时看见篮子里有数据,直接拿走就行。该过程不再阻塞
  • PriorityBlockingQueue:底层维护了一个基于数组的小顶堆,需传入一个参数作为可容纳任务数量的上界,如果不传则使用默认容量11
  • DelayQueue:底层维护了一个PriorityQueue,优先级按元素的延迟时间决定,延迟时间小的元素放在堆顶。常用来实现延时任务,比如60s后给用户发短信,订单超过30分钟用户未付款则自动取消订单。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值