笔记标题 | 青训营笔记

47 阅读3分钟

这是我参与「第四届青训营 」笔记创作活动的第9天

聊聊线程池

为什么要使用线程池

  1. 减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 可以根据系统的承受能力,调整线程池中工作线程的数目,放置因为消耗过多的内存,而把服务器累趴下。

核心线程池ThreadPoolExecutor内部参数

  1. corePoolSize:指定了线程池中的线程数量
  2. maximumPoolSize:指定了线程池中的最大线程数量
  3. keepAliveTime:线程池维护线程所允许的空闲时间
  4. unit: keepAliveTime 的单位。
  5. workQueue:任务队列,被提交但尚未被执行的任务。
  6. threadFactory:线程工厂,用于创建线程,一般用默认的即可。
  7. handler:拒绝策略。当任务太多来不及处理,如何拒绝任务。

线程池的执行流程

  1. 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务
  2. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列
  3. 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务
  4. 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常RejectExecutionException。

线程池有哪几种工作队列

  1. ArrayBlockingQueue:底层是数组,有界队列,如果我们要使用生产者-消费者模式,这是非常好的选择。
  2. LinkedBlockingQueue:底层是链表,可以当做无界和有界队列来使用,所以大家不要以为它就是无界队列。
  3. SynchronousQueue:本身不带有空间来存储任何元素,使用上可以选择公平模式和非公平模式。
  4. PriorityBlockingQueue:无界队列,基于数组,数据结构为二叉堆,数组第一个也是树的根节点总是最小值。
  • 举例 ArrayBlockingQueue 实现并发同步的原理:原理就是读操作和写操作都需要获取到 AQS 独占锁才能进行操作。如果队列为空,这个时候读操作的线程进入到读线程队列排队,等待写线程写入新的元素,然后唤醒读线程队列的第一个等待线程。如果队列已满,这个时候写操作的线程进入到写线程队列排队,等待读线程将队列元素移除腾出空间,然后唤醒写线程队列的第一个等待线程。

线程池的拒绝策略

  1. ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
  2. ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
  3. ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
  4. ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务。

线程池的正确创建方式

不能用Executors,newFixed和newSingle,因为队列无限大,容易造成耗尽资源和OOM,newCached和newScheduled最大线程数是Integer.MAX_VALUE,线程创建过多和OOM。应该通过ThreadPoolExecutor手动创建。

线程提交submit()和execute()有什么区别

  1. submit()相比于excute(),支持callable接口,也可以获取到任务抛出来的异常
  2. 可以获取到任务返回结果
  3. 用submit()方法执行任务,用Future.get()获取异常

线程池的线程数量怎么确定

  1. 一般来说,如果是CPU密集型应用,则线程池大小设置为N+1。
  2. 一般来说,如果是IO密集型应用,则线程池大小设置为2N+1。
  3. 在IO优化中,线程等待时间所占比例越高,需要越多线程,线程CPU时间所占比例越高,需要越少线程。这样的估算公式可能更适合:最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目。