这是我参与「第四届青训营 」笔记创作活动的第9天
聊聊线程池
为什么要使用线程池
- 减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 可以根据系统的承受能力,调整线程池中工作线程的数目,放置因为消耗过多的内存,而把服务器累趴下。
核心线程池ThreadPoolExecutor内部参数
- corePoolSize:指定了线程池中的线程数量
- maximumPoolSize:指定了线程池中的最大线程数量
- keepAliveTime:线程池维护线程所允许的空闲时间
- unit: keepAliveTime 的单位。
- workQueue:任务队列,被提交但尚未被执行的任务。
- threadFactory:线程工厂,用于创建线程,一般用默认的即可。
- handler:拒绝策略。当任务太多来不及处理,如何拒绝任务。
线程池的执行流程
- 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务
- 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列
- 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务
- 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常RejectExecutionException。
线程池有哪几种工作队列
- ArrayBlockingQueue:底层是数组,有界队列,如果我们要使用生产者-消费者模式,这是非常好的选择。
- LinkedBlockingQueue:底层是链表,可以当做无界和有界队列来使用,所以大家不要以为它就是无界队列。
- SynchronousQueue:本身不带有空间来存储任何元素,使用上可以选择公平模式和非公平模式。
- PriorityBlockingQueue:无界队列,基于数组,数据结构为二叉堆,数组第一个也是树的根节点总是最小值。
- 举例 ArrayBlockingQueue 实现并发同步的原理:原理就是读操作和写操作都需要获取到 AQS 独占锁才能进行操作。如果队列为空,这个时候读操作的线程进入到读线程队列排队,等待写线程写入新的元素,然后唤醒读线程队列的第一个等待线程。如果队列已满,这个时候写操作的线程进入到写线程队列排队,等待读线程将队列元素移除腾出空间,然后唤醒写线程队列的第一个等待线程。
线程池的拒绝策略
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务。
线程池的正确创建方式
不能用Executors,newFixed和newSingle,因为队列无限大,容易造成耗尽资源和OOM,newCached和newScheduled最大线程数是Integer.MAX_VALUE,线程创建过多和OOM。应该通过ThreadPoolExecutor手动创建。
线程提交submit()和execute()有什么区别
- submit()相比于excute(),支持callable接口,也可以获取到任务抛出来的异常
- 可以获取到任务返回结果
- 用submit()方法执行任务,用Future.get()获取异常
线程池的线程数量怎么确定
- 一般来说,如果是CPU密集型应用,则线程池大小设置为N+1。
- 一般来说,如果是IO密集型应用,则线程池大小设置为2N+1。
- 在IO优化中,线程等待时间所占比例越高,需要越多线程,线程CPU时间所占比例越高,需要越少线程。这样的估算公式可能更适合:最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目。