【线程池】深入理解线程池

每天学习一个小知识

什么是线程池

线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。其技术的核心思想其实就是实现资源的一个复用,避免资源的重复创建和销毁带来的性能开销。在线程池中,线程池可以管理一堆线程,让线程执行完任务之后不会进行销毁,而是继续去处理其它线程已经提交的任务。

线程池生命周期

1、新建状态(NEW)

当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值

2、就绪状态(RUNNABLE)

当线程对象调用了 start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行

3、运行状态(RUNNING)

如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态。

4、阻塞状态(BLOCKED)

阻塞状态是指线程因为某种原因让出了 cpu 使用权,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状态。阻塞的情况分三种:

(1)等待阻塞(wait->等待对列):

运行(running)的线程执行 wait()方法,JVM 会把该线程放入等待队列(waitting queue) 中。

(2)同步阻塞(lock->锁池) :

运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。

(3)其他阻塞(sleep/join) :

运行(running)的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入可运行(runnable)状态。

5、线程死亡(DEAD)

(1)正常结束

run()或 call()方法执行完成,线程正常结束。

(2)异常结束

线程抛出一个未捕获的 Exception 或 Error。

(3)调用 stop

直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。

【线程池】深入理解线程池_第1张图片

线程池工作原理

1、线程池刚创建时,里面是没有线程的。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。

2、当调用 execute() 方法添加一个任务时,线程池会做如下判断:

(1)如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;

(2)如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;

(3)如果这时候队列满了,而且正在运行的线程数量小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;

(4)如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,则线程池会抛出异常RejectExecutionException。

3、当一个线程完成任务时,它会从队列中取下一个任务来执行。

4、当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

【线程池】深入理解线程池_第2张图片

线程池的实现方式

1、继承 Thread 类

启动线程是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法。

2、实现 Runnable 接口

如果类已经继承了父类了,那么久就不能再继承 Thread 类,我们可以实现Runnable接口。(无返回值)

3、实现 Callable 接口

执行Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object 了。(有返回值)

线程池七大参数

1、corePoolSize: 核心线程数

核心线程大小,线程池中维护了一个最小线程数量,即使这些线程处于空闲状态,也一直存在线程池中,除非设置了核心线程超时时间


2、maximumPoolSize: 最大线程数

线程池中允许的最大线程数量。当线程池中核心线程都处理执行状态,有新请求的任务:

(1)、工作队列未满:新请求的任务加入工作队列

(2)、工作队列已满:线程池会创建新线程,来执行这个任务。当然,创建新线程不是无限制的,因为会受到maximumPoolSize最大线程数量的限制。


3、keepAliveTime: 最大空闲时间

空闲线程存活时间。具体说,当线程数大于核心线程数时,空闲线程在等待新任务到达的最大时间,如果超过这个时间还没有任务请求,该空闲线程就会被销毁。


4、TimeUnit unit: 时间单位

空闲线程存活时间的单位。keepAliveTime的计量单位。枚举类型TimeUnit类。存活时间通常设置为 60s 左右,即当线程池中的线程空闲时间超过了 60 秒,那么这个线程就会被回收。同时,当线程池中的线程数量小于等于核心线程池大小时,存活时间将不起作用。


5、BlockingQueue workQueue: 任务队列

任务队列是存储被提交但尚未被执行的任务的阻塞队列。常用的任务队列有如下几类:

  • ArrayBlockingQueue:基于数组的有限队列,可以指定容量。
  • LinkedBlockingQueue:基于链表的无限队列,可以无限扩展。
  • PriorityBlockingQueue:优先级队列,可以自定义排列顺序。
  • SynchronousQueue:同步队列,不存储数据,只在提交和取出数据时传递数据。

6、ThreadFactory threadFactory: 线程工厂

用来创建线程工厂的类,在线程池中需要创建新线程时使用 threadFactory(创建线程工厂) 进行创建线程对象,可以通过自定义 threadFactory(创建线程工厂)对线程进行个性化配置和定制,例如设置线程的名称、优先级等,可以更好的跟踪和管理线程池中的线程


7、RejectedExecutionHandler handler: 拒绝策略

拒绝策略是当任务队列满了需要执行拒绝策略来处理新提交的任务。提供了几种预定义的拒绝策略

  • AbortPolicy:直接抛出异常,默认策略。
  • CallerRunsPolicy:主线程执行该任务。
  • DiscardOldestPolicy:丢弃队列中最老的任务,然后重新尝试执行当前任务。
  • DiscardPolicy:默默丢弃提交的任务,没有异常。

线程池及线程池大小调优技巧

1、核心线程池数量调优

核心线程池数量不宜过多,因为每个线程都需要占用内存和 CPU 资源,过多的核心线程池数量会导致系统资源的浪费,从而降低系统性能。但也不应过少,否则会降低系统吞吐量。

2、最大线程池数量调优

最大线程池数量一般设置:最大线程池数量 = CPU 核心数 + 网络连接数 + 其他 IO 等待时间的线程数量

3、存活时间调优

设定一个适当的线程存活时间,可以有效地减少线程的创建和销毁带来的性能开销。在存活时间到达之后,多余的线程会被回收,从而释放系统资源。

4、队列调优

任务队列是存储被提交但尚未被执行的任务的阻塞队列。在选择队列类型时,应考虑任务数量和任务类型,以及需要处理的并发请求数。常用的阻塞队列有 ArrayBlockingQueue 和 LinkedBlockingQueue。

5、拒绝策略调优

拒绝策略通常分为四种:AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy 和 DiscardPolicy。根据业务需求和系统负载情况,选择合适的拒绝策略。

应用场景

1、CPU 密集型应用线程池设置

在 CPU 密集型应用中,任务主要是 CPU 计算,线程池的大小应该根据 CPU 核心数来设置,以充分利用 CPU 资源,并避免过多线程间的竞争和上下文切换。通常情况下,将核心线程池大小设置为 CPU 核心数,将最大线程池大小设置为 CPU 核心数 * 2。比如,当前服务器有 8 核 CPU,那么推荐设置核心线程池大小为 8,最大线程池大小为 16。

2、IO 密集型应用线程池设置

在 IO 密集型应用中,任务主要是从事 IO 等待,而线程的 CPU 计算能力却很小,此时线程数量适当多一点,可以让 CPU 等待 IO 的数目更多,以充分利用计算机的硬件资源。通常情况下,核心线程池大小可以设置为 CPU 核心数 +1,最大线程池大小可以设置为 CPU 核心数 * 2。同时,建议使用无界的 LinkedBlockingQueue 阻塞队列,以避免丢失任务。

3、其它应用场景

对于各种不同的场景,应该根据实际情况进行参数的设置。比如生产者-消费者问题,可以使用 FixedThreadPool,保证消费者线程数量少于核心线程池大小,以确保消费者线程能够及时执行。同时,阻塞队列也可以根据实际情况选择不同的存储方式。对于需要开多个线程处理的应用,可以使用 ScheduledThreadPoolExecutor,定时执行任务。总之,根据实际需求来设置线程池的参数是最重要的。

你可能感兴趣的:(java,jvm,开发语言)