小白一文搞懂线程池

77 阅读5分钟

为什么使用线程池?线程池的作用

  1. 降低资源消耗:提高线程利用率,降低创建和销毁线程的消耗。
  2. 提高响应速度:任务来了,直接拿线程池的的线程可用可执行,而不是先创建线程,再执行。
  3. 提高线程的可管理性:线程是稀缺资源,使用线程池可以统一分配销毁。

PS:可以类比我们打网球的场景,线程池是装网球的框子,线程是一个个网球,每次训练打网球时会在筐子里准备一筐子网球,每次用一个拿一个,即用即拿,用完之后统一回收

线程池有哪些状态?

  • Running:这是最正常的状态,可接收新任务,也可处理等待队列中的任务
  • ShutDown: 不再接收新任务的提交,但会继续处理等待队列中的任务。
  • Stop: 不接收新任务的提交,也不处理等待队列中的任务,中断正在执行任务的线程。
  • Tidying: 所有任务都销毁了,workcount为0,线程池状态在转换为tidying状态时,会执行钩子方法terminated()
  • Terminated: terminated()方法执行结束后,线程池的状态就会变成这个。

线程池有哪些参数?

  • corePoolSize: 核心线程数,也就是正常情况下能创建的线程数,这些线程创建后并不会消除,而是一种常驻线程。
  • workQueue: 阻塞队列,用来放等待执行的任务。假设核心线程数被使用完,还有任务进来放队列里,若队列放满则开始创建救急线程。
  • maximumPoolSize: 最大线程数目。(注:最大线程数=核心线程数+救急线程数)
  • keepALiveTime:生存时间(针对救急线程),超出生存时间的非核心线程数会被消除。
  • unit:生存时间的时间单位,针对救急线程
  • ThreadFactory: 线程工厂,用来生产线程执行任务。我们可以使用默认的线程工厂:产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。我们也可以自定义线程工厂,可以根据业务制定不同的线程工厂。
  • Handler: 任务拒绝策略

任务拒绝策略有哪些?

  • AbortPolicy(默认策略): 让调用者抛出rejectExecutionException异常
  • CallerRunsPolicy: 让调用者运行任务
  • DiscardPolicy: 放弃本次任务
  • DiscardOldestPolicy: 放弃队列中最早的任务取而代之

线程池的处理流程具体是怎样的?

image.png

  • 线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。
  • 当线程数达到 corePoolSize 并没有线程空闲时,这时再加入任务,新加的任务会被加入 workqueen队列排队,直到有空闲的线程。
  • 如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线程来救急。
  • 如果救急线程数也达到上限,这时再来任务会执行 拒绝策略。

阻塞队列的作用是什么?

一般队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务。 阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。 阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活,不至于一直占用cpu资源。

为什么新任务来了是先添加队列而不是先创建最大线程?

在创建线程的时候,是要获取全局锁的,这个时候其他的就得阻塞,影响了整体效率。 就好比工厂企业里有10个(core)正式工的名额,最多招10个正式工,要是人数超过正式工人数(take>core)的情况下,工厂领导(线程池)不是先扩招工人,还是这10个人,但是任务可以稍微积压一下,即先放到队列去(代价低)。10个正式工慢慢干,迟早会干完的,要是任务还在继续增加,超过正式工加班忍耐极限了,就找外包(救急线程)帮忙了。要是正式工加外包还是不能完成任务,那新来的任务也会被领导拒绝。

创建线程池有哪几种方式?

  • newFiexedThreadPool

核心线程数=最大线程数,没有救急线程被创建,因此也无需超时时间;阻塞队列是无界的,可以放任意数量的任务。

总结:适用于任务量已知,相对耗时的任务

@Slf4j
public class ThreadPoolTest {
    public static void main(String[] args) {
        ExecutorService poolabc = Executors.newFixedThreadPool(3);

        poolabc.execute(()->log.info("1"));
        poolabc.execute(()->log.info("2"));
        poolabc.execute(()->log.info("3"));
    }
}
  • newCachedThreadPool

核心线程数是0,最大线程数是Integer.MAX_VALUE,救急线程的空闲生存时间是60s,意味着全部都是救急线程(60s后可以回收),救急线程可以无限创建 队列采用了SynchronousQUeue,实现特点是它没有容量,没有线程来取是放不进去的

总结:整个线程池表现为线程数根据任务量不断增长,没有上限,当任务执行完毕,空闲一分钟后释放线程。 适用于任务数比较密集,但每个任务执行时间比较短的情况。

  • newSingleThreadPool 只有一个核心线程。任务多于1时,会放入无界队列排队,任务执行完毕,唯一的线程也不会释放。

若自己创建一个单线程执行任务和newSingleThreadPool线程池执行任务的区别?

如果自己创建的任务执行失败而终止,那么没有任何补救措施,而线程池还会创建一个线程,保证池的正常工作。

newScheduledThreadPool schedule可以延迟执行任务

ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
log.info("start..");
pool.schedule(()->{
    try{
        TimeUnit.SECONDS.sleep(2);
        log.info("1");
    }catch (InterruptedException e){
        e.printStackTrace();
    }
}, 1, TimeUnit.SECONDS);
pool.schedule(()->{
    log.info("2");
}, 1, TimeUnit.SECONDS);

运行结果截图

image.png

误用线程池带来的问题

可能导致内存溢出