线程池

什么是线程池

线程池是一种多线程处理形式,在处理过程中将任务添加到队列中,然后在创建线程后自动启动这些任务。比如把线程池看成一个容器,集中管理线程。线程使用完不会销毁,会先储存在线程池中。

线程池的工作原理

在这里插入图片描述

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
  • corePoolSize:核心池的大小,在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中
  • maximumPoolSize:线程池最大线程数最大线程数
  • keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止
  • unit:参数keepAliveTime的时间单位TimeUtil类的枚举类(DAYS、HOURS、MINUTES、SECONDS 等)
  • workQueue:阻塞队列,用来存储等待执行的任务
  • threadFactory:线程工厂,主要用来创建线程
  • handler:拒绝处理任务的策略
    • AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常。(默认这种)
    • DiscardPolicy:也是丢弃任务,但是不抛出异常
    • DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
    • CallerRunsPolicy:由调用线程处理该任务

当线程池中有任务需要执行时,线程池会判断如果线程数量没有超过核心数量就会新建线程池进行任务执行,如果线程池中的线程数量已经超过核心线程数,这时候任务就会被放入任务队列中排队等待执行。

  • 当任务队列为无界队列时,任务就会一直放入缓冲的任务队列中,不会和最大线程数量进行比较。
  • 当任务队列为有界队列时,任务先放入缓冲的任务队列中,当任务队列满了之后,才会将任务放入线程池,如果线程池没有达到最大线程数,就会新建线程来执行任务;如果超过了最大线程数,就会执行拒绝执行策略,默认会抛出异常。当任务执行完,又会将缓冲队列中的任务放入线程池中,然后重复此操作。

线程池的作用

  1. 降低资源消耗:通过重用已经创建的线程来降低线程创建和销毁的消耗。
  2. 提高响应速度:任务到达时不需要等待线程创建就可以立即执行。
  3. 提高线程的可管理性:线程池可以统一管理、分配、调优和监控。

线程池采用预创建的技术,在应用程序启动之后,将立即创建一定数量的线程(N1),放入空闲队列中。这些线程都是处于阻塞(Suspended)状态,不消耗CPU,但占用较小的内存空间。当任务到来后,缓冲池选择一个空闲线程,把任务传入此线程中运行。当N1个线程都在处理任务后,缓冲池自动创建一定数量的新线程,用于处理更多的任务。在任务执行完毕后线程也不退出,而是继续保持在池中等待下一次的任务。当系统比较空闲时,大部分线程都一直处于暂停状态,线程池自动销毁一部分线程,回收系统资源。基于这种预创建技术,线程池将线程创建和销毁本身所带来的开销分摊到了各个具体的任务上,执行次数越多,每个任务所分担到的线程本身开销则越小,不过我们另外可能需要考虑进去线程之间同步所带来的开销
事实上,线程池并不是万能的。它有其特定的使用场合。线程池致力于减少线程本身的开销对应用所产生的影响,这是有前提的,前提就是线程本身开销与线程执行任务相比不可忽略。如果线程本身的开销相对于线程任务执行开销而言是可以忽略不计的,那么此时线程池所带来的好处是不明显的,比如对于FTP服务器以及Telnet服务器,通常传送文件的时间较长,开销较大,那么此时,我们采用线程池未必是理想的方法,我们可以选择“即时创建,即时销毁”的策略。
总之线程池通常适合下面的几个场合:

(1) 单位时间内处理任务频繁而且任务处理时间短;
(2) 对实时性要求较高。如果接受到任务后在创建线程,可能满足不了实时要求,因此必须采用线程池进行预创建。

线程池中核心线程数量大小怎么设置

「CPU密集型任务」:比如像加解密,压缩、计算等一系列需要大量耗费 CPU 资源的任务,大部分场景下都是纯 CPU 计算。尽量使用较小的线程池,一般为CPU核心数+1。因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。

「IO密集型任务」:比如像 MySQL 数据库、文件的读写、网络通信等任务,这类任务不会特别消耗 CPU 资源,但是 IO 操作比较耗时,会占用比较多时间。可以使用稍大的线程池,一般为2*CPU核心数。IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。

另外:线程的平均工作时间所占比例越高,就需要越少的线程;线程的平均等待时间所占比例越高,就需要越多的线程

以上只是理论值,实际项目中建议在本地或者测试环境进行多次调优,找到相对理想的值大小。

线程池为什么需要使用(阻塞)队列

  1. 因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM(Out Of Memory),并且会造成cpu过度切换。
  2. 创建线程池的消耗较高。

线程池为什么要使用阻塞队列而不使用非阻塞队列

阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。使得在线程不至于一直占用cpu资源。(线程执行完任务后通过循环再次从任务队列中取出任务进行执行,代码片段如下while (task != null || (task = getTask()) != null) {})。

不用阻塞队列也是可以的,不过实现起来比较麻烦。

关于阻塞队列和非阻塞的队列的讲解参见这篇文章.。

几种常见的线程池及使用场景

常见的线程池有四种。

1.newFixedThreadPool 创建一个定长线程池,可控制最大并发数,超出的线程将在队列中等待。

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

该线程池特点:

  • 核心线程数和最大线程数大小一样
  • keepAliveTime为0
  • 阻塞队列是LinkedBlockingQueue

该线程池的工作机制是:

  • 线程数少于核心线程数,也就是设置的线程数时,新建线程执行任务
  • 线程数等于核心线程数后,将任务加入阻塞队列。
    由于队列容量非常大(Integer.MAX_VALUE),可以一直加。(当线程池中的任务比较特殊时,比如关于数据库的长时间的IO操作,可能导致OOM)
  • 执行完任务的线程反复去队列中取任务执行

适用场景:

FixedThreadPool 适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程即可。一般Ncpu+1。

2.newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有的任务按照指定顺序执行(FIFO,LIFO)。

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

该线程池特点:

  • 核心线程数和最大线程数大小一样且都是1
  • keepAliveTime为0
  • 阻塞队列是LinkedBlockingQueue

该线程池的工作机制是:

  • 线程池中没有线程时,新建一个线程执行任务
  • 有一个线程以后,将任务加入阻塞队列,不停的加
  • 唯一的这一个线程不停地去队列里取任务执行

适用场景:

SingleThreadExecutor适用于串行执行任务的场景,每个任务必须按顺序执行,不需要并发执行

3.newCachedThreadPool 创建一个可缓存的线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

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

该线程池特点:

  • 核心线程数为0,且最大线程数为Integer.MAX_VALUE
  • 阻塞队列是SynchronousQueue

SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue

当提交任务的速度大于处理任务的速度时,每次提交一个任务,就必然会创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。由于空闲 60 秒的线程会被终止,长时间保持空闲的 CachedThreadPool 不会占用任何资源。

该线程池的工作机制是:

  • 没有核心线程,直接向SynchronousQueue中提交任务
  • 如果有空闲线程,就去取出任务执行;如果没有空闲线程,就新建一个
  • 执行完任务的线程有60秒生存时间,如果在这个时间内可以接到新任务,就可以继续活下去,否则就结束

适用场景:

CachedThreadPool 用于并发执行大量短期的小任务

4.newScheduledThreadPool 创建一个定时线程池,支持定时及周期性的执行任务。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}


public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue());
}

private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;

该线程池特点:

  • 最大线程数为Integer.MAX_VALUE
  • 阻塞队列是DelayedWorkQueue

ScheduledThreadPoolExecutor 添加任务提供了另外两个方法:

scheduleAtFixedRate() :按某种速率周期执行
scheduleWithFixedDelay():在某个延迟后执行

该线程池的工作机制是:

  • 调用上面两个方法添加一个任务
  • 线程池中的线程从 DelayQueue 中取任务,然后执行任务

适用场景:

ScheduledThreadPoolExecutor用于需要多个后台线程执行周期任务的场景。

单机上一个线程正在处理服务,如果忽然断电了怎么办(正在处理的请求和阻塞队列里的请求怎么处理)

实现思路和MySQL的redo,undo功能很相似,我们可以对正在处理和阻塞队列的任务做事物管理或者对阻塞队列中的任务持久化处理,并且当断电或者系统崩溃,操作无法继续下去的时候,可以通过回溯日志的方式来撤销正在处理的已经执行成功的操作,然后重新执行整个阻塞队列。

即:阻塞队列持久化,正在处理事物控制。通过事务回滚和日志,恢复断电之前正在处理的操作。通过持久化,服务器重启后阻塞队列中的数据能够重新加载。

线程池中线程复用原理

线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的一个线程必须对应一个任务的限制。

在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对 Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停的检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式将只使用固定的线程就将所有任务的 run 方法串联起来。

线程池状态

通过获取线程池状态,可以判断线程池是否是运行状态、可否添加新的任务以及优雅地关闭线程池等。
在这里插入图片描述

  • RUNNING:线程池的初始化状态,可以添加待执行的任务。
  • SHUTDOWN:线程池处于待关闭状态,不接收新任务仅处理已经接收的任务。
  • STOP:线程池立即关闭,不接收新的任务,放弃缓存队列中的任务并且中断正在处理的任务。
  • TIDYING:线程池自主整理状态,调用 terminated() 方法进行线程池整理。
  • TERMINATED:线程池终止状态。

ThreadPoolExecutor 有哪些常用的方法

java的线程池支持主要通过ThreadPoolExecutor来实现,ThreadPoolExecutor有如下常用方法:

  • submit()/execute():执行线程池
  • shutdown()/shutdownNow():终止线程池
  • isShutdown():判断线程是否终止
  • getActiveCount():正在运行的线程数
  • getCorePoolSize():获取核心线程数
  • getMaximumPoolSize():获取最大线程数
  • getQueue():获取线程池中的任务队列
  • allowCoreThreadTimeOut(boolean):设置空闲时是否回收核心线程

这些方法可以用来终止线程池、线程池监控等

submit(和 execute两个方法的区别:

submit() 和 execute() 都是用来执行线程池的,只不过使用 execute() 执行线程池不能有返回方法,而使用 submit() 可以使用 Future 接收线程池执行的返回值。

shutdownNow() 和 shutdown() 两个方法的区别:

shutdownNow() 和 shutdown() 都是用来终止线程池的,它们的区别是,使用 shutdown() 程序不会报错,也不会立即终止线程,它会等待线程池中的缓存任务执行完之后再退出,执行了 shutdown() 之后就不能给线程池添加新任务了;shutdownNow() 会试图立马停止任务,如果线程池中还有缓存任务正在执行,则会抛出 java.lang.InterruptedException: sleep interrupted 异常。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值