无痛上手!!论一个Java程序员的自我修养-线程池

简述

线程池大家在开发中应该有都有用过,其实线程池就是把一堆线程创建好了放在一个容器中,需要用的时候就直接拿出来用,用完之后再放回池子里。

那线程池有什么好处呢?

  • 低资源消耗,降低了频繁创建线程和销毁线程的开销
  • 提高响应速度
  • 提高线程的可管理性,可以对线程进行一些操作,方便管理线程

线程池的优势

合理利用线程池能够带来三个好处:

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗;
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行;
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池的创建

我们可以通过java.util.concurrent.ThreadPoolExecutor来创建一个线程池:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) 

线程池的主要参数:

  • corePoolSize:核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会销毁,而是一种常驻线程;
  • maxinumPoolSize:最大线程数,它与核心线程数相对应,表示最大允许被创建的线程数,比如当前任务较多,将核心线程数都用完了,还无法满足需求时,此时就会创建新的线程,但是线程池内线程总数不会超过最大线程数;
  • keepAliveTime unit:表示超出核心线程数之外的线程的空闲存活时间,也就是核心线程不会消除,但是超出核心线程数的部分线程如果空闲一定的时间则会被消除,我们可以通过 setKeepAliveTime 来设置空闲时间。
  • workQueue:用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进来则全部放入队列,直到整个队列被放满但任务还再持续进入则会开始创建新的线程;
  • ThreadFactory:实际上是一个线程工厂,用来生产线程执行任务。我们可以选择使用默认的创建工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择自定义线程工厂,一般我们会根据业务来制定不同的线程工厂;
  • Handler:任务拒绝策略,有两种情况,第一种是当我们调用 shutdown 等方法关闭线程池后,这时候即使线程池内部还有没执行完的任务正在执行,但是由于线程池已经关闭,我们再继续想线程池提交任务就会遭到拒绝。另一种情况就是当达到最大线程数,线程池已经没有能力继续处理新提交的任务时,这是也就拒绝。

常见的阻塞队列BlockingQueue

  • ArrayBlockingQueue:一个基于数组来实现的有界阻塞队列,按照 FIFO 原则对元素进行排序;
  • LinkedBlockingQueue:一个基于链表来实现的无界阻塞队列,此队列按 FIFO 排序元素(队列的长度是 Integer.MAX_VALUE,值非常大,几乎无法达到,对此可以认为属于无界队列);
  • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态;
  • PriorityBlockingQueue:一个具有优先级的无界阻塞队列。

线程池的拒绝策略

线程池的拒绝策略是用来处理当线程池无法继续处理更多的任务时的处理机制。线程池的拒绝时机有两种:

  1. 当我们正常关闭线程池时也就是使用 shutdown 等方法,这时候即使线程池中还有未完成的任务正在执行,但是由于线程池已经关闭,所以这时候我们再继续向线程池提交任务的话就会被拒绝。
  2. 当线程池没有能力再继续处理提交的任务,如队列已满,最大线程数达到,这时候线程池就处于饱和状态。

拒绝策略

  • AbortPolicy:当任务添加到线程池中被拒绝时,它将抛出 RejectedExecutionException 异常(默认策略);
  • CallerRunsPolicy:当任务添加到线程池中被拒绝时,会在线程池当前正在运行的Thread线程池中处理被拒绝的任务;
  • DiscardOldestPolicy:当任务添加到线程池中被拒绝时,线程池会放弃等待队列中最旧的未处理任务,然后将被拒绝的任务添加到等待队列中;
  • DiscardPolicy:当任务添加到线程池中被拒绝时,线程池将丢弃被拒绝的任务。

向线程池提交任务

  1. 使用execute提交任务,但是 execute 方法没有返回值,所以无法判断任务知否被线程池执行成功;
  2. 使用submit方法来提交任务,它会返回一个 future,可以判断任务是否执行成功。通过future的get方法来获取返回值,get方法会阻塞住直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时有可能任务没有执行完。

线程池的关闭

我们可以通过调用线程池的shutdownshutdownNow方法来关闭线程池,但是它们的实现原理不同:

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

只要调用了这两个关闭方法的其中一个,isShutdown 方法就会返回 true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用 isTerminaed 方法会返回 true。至于我们应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用 shutdown 来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow。

线程池的流程分析

image.png

常见的线程池

在Java中常见的线程池:FixedThreadPool(核心线程数与最大线程数相同)、SingleThreadExecutor(一个线程的线程池)和CachedThreadPool(核心线程为0,最大线程数为Integer. MAX_VALUE)。

FixedThreadPool

FixedThreadPool 线程池的特点是它的核心线程数和最大线程数一样,我们可以看它的实现代码在 Executors#newFixedThreadPool(int) 中,如下:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
  • 这种线程池我们可以看作是固定线程数的线程池,它只有在开始初始化的时候线程数会从0开始创建,但是创建好后就不再销毁,而是全部作为常驻线程池。
  • 对于这种线程池他的第三个和第四个参数是没意义,它们是空闲线程存活时间,这里都是常驻不存在销毁,当线程处理不了时会加入到阻塞队列,这是一个链表结构的有界阻塞队列,最大长度是Integer. MAX_VALUE。

SingleThreadExecutor

SingleThreadExecutor 线程的特点是它的核心线程数和最大线程数均为1,我们也可以将其任务是一个单例线程池,它的实现代码是Executors#newSingleThreadExcutor() , 如下:

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>(),
                                threadFactory));
}
  • 上述代码中我们发现它有一个重载函数,传入了一个ThreadFactory 的参数,一般在我们开发中会传入我们自定义的线程创建工厂,如果不传入则会调用默认的线程工厂
  • 我们可以看到它与 FixedThreadPool 线程池的区别仅仅是核心线程数和最大线程数改为了1,也就是说不管任务多少,它只会有唯一的一个线程去执行
  • 如果在执行过程中发生异常等导致线程销毁,线程池也会重新创建一个线程来执行后续的任务
  • 这种线程池非常适合所有任务都需要按被提交的顺序来执行的场景,是个单线程的串行。

cachedThreadPool

cachedThreadPool 线程池的特点是它的常驻核心线程数为0,正如其名字一样,它所有的县城都是临时的创建,关于它的实现在 Executors#newCachedThreadPool() 中,代码如下:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>(),
                                  threadFactory);
}
  • 从上述代码中我们可以看到 CachedThreadPool 线程池中,最大线程数为 Integer.MAX_VALUE , 意味着他的线程数几乎可以无限增加。
  • 因为创建的线程都是临时线程,所以他们都会被销毁,这里空闲 线程销毁时间是60秒,也就是说当线程在60秒内没有任务执行则销毁
  • 这里我们需要注意点,它使用了 SynchronousQueue 的一个阻塞队列来存储任务,这个队列是无法存储的,因为他的容量为0,它只负责对任务的传递和中转,效率会更高,因为核心线程都为0,这个队列如果存储任务不存在意义。

合理配置线程池

要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:

  1. 任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
  2. 任务的优先级:高,中和低。
  3. 任务的执行时间:长,中和短。
  4. 任务的依赖性:是否依赖其他系统资源,如数据库连接。

 

最后

给大家分享一篇一线开发大牛整理的java高并发核心编程神仙文档,里面主要包含的知识点有:多线程、线程池、内置锁、JMM、CAS、JUC、高并发设计模式、Java异步回调、CompletableFuture类等。

领取文档地址:GitHub标星12.5K+Java高并发核心编程知识笔记助我提升,感觉之前学的都是渣渣

感谢阅读,文章对你有帮助的话,不妨 一键三连 支持一下吧。你们的支持是我最大的动力,祝大家万事胜意!

 

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值