Java线程(线程池)

线程池

 

什么是线程池

 

线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池可以缓存线程,可用已有的闲置线程来执行新任务。

 

线程池的优势

总体来说,线程池有如下的优势:

(1)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

(2)提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

(3)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

 

 

创建线程池的方式

创建线程池的方式有六种

  1. newCachedThreadPool

  2. newFixedThreadPool

  3. newScheduledThreadPool

  4. newSingleThreadExecutor

  5. ThreadPoolExecutor

  6. ThreadPoolTaskExecutor

 

其中,前四种是由Executor提供的静态工厂方法;

 

newCachedThreadPool

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

线程池中没有核心线程数,当一个任务提交过来时,如果有线程池中没有空闲的线程,而SynchronousQueue是无法存放数据的,所以会创建一个新的线程来执行这个任务,当线程池中的线程60s内还没有获取到任务就会自动销毁。

newFixedThreadPool

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

创建一个线程池,该线程池重用固定数量的线程,这些线程在共享无界队列上操作。

在任何时候,最多nThreads线程将是活动的处理任务。

如果在所有线程都处于活动状态时提交了额外的任务,它们将在队列中等待,直到有一个线程可用。

如果任何线程在关闭之前的执行过程中由于失败而终止,如果需要执行后续任务,将会有一个新的线程取代它的位置。在显式关闭池之前,池中的线程将一直存在。

返回:新创建的线程池抛出:illegalargumentexception -如果nThreads <= 0

 

 

newScheduledThreadPool

是一个可以周期性执行任务的线程池;

特点:延时启动  、定时启动  、可以自定义最大线程池数量

java.util.concurrent.Executors#newScheduledThreadPool(int)

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

 

java.util.concurrent.ScheduledThreadPoolExecutor#ScheduledThreadPoolExecutor(int)

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

 

 

newSingleThreadExecutor

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

 

只会启用一个核心线程,使用阻塞队列的是无界队列

和单线程执行任务的区别:

自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池会将异常捕获存放到FutureTask中,然后继续执行下一个任务,当调用Future.get()时才会抛出异常,不会影响下一个任务的执行。

 

 

自定义线程池ThreadPoolExecutor

七个参数:

  1. corePoolSize(线程池基本大小): 当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,(除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。)

  2. maximumPoolSize(线程池最大大小): 线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。

  3. keepAliveTime(线程存活保持时间): 当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。

  4. TimeUnit(存活时间单位):一般设置为毫秒或秒

  5. workQueue:任务队列,被添加到线程池中,但尚未被执行的任务;它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列几种;
  6. threadFactory:线程工厂,用于创建线程,一般用默认即可;
  7. handler:拒绝策略;当任务太多来不及处理时,如何拒绝任务;

 

 

workQueue任务队列

workQueue一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列、阻塞/非阻塞队列;

名字中包含 BlockingQueue 关键字的一般都是阻塞队列,如:ArrayBlockingQueue、LinkedBlockingQueue

 

直接提交队列

设置为SynchronousQueue队列,SynchronousQueue是一个特殊的BlockingQueue,它没有容量,每执行一个插入操作就会阻塞,需要再执行一个删除操作才会被唤醒,反之每一个删除操作也都要等待对应的插入操作。

 

ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, new SynchronousQueue<Runnable>(), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

当任务队列为SynchronousQueue,创建的线程数大于maximumPoolSize时,直接执行了拒绝策略抛出异常。

使用SynchronousQueue队列,提交的任务不会被保存,总是会马上提交执行。如果用于执行任务的线程数量小于maximumPoolSize,则尝试创建新的进程,如果达到maximumPoolSize设置的最大值,则根据你设置的handler执行拒绝策略。因此这种方式你提交的任务不会被缓存起来,而是会被马上执行,在这种情况下,你需要对你程序的并发量有个准确的评估,才能设置合适的maximumPoolSize数量,否则很容易就会执行拒绝策略;

 

 

有界的任务队列

有界的任务队列可以使用ArrayBlockingQueue实现

 

ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(10),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());

 

使用ArrayBlockingQueue有界任务队列,若有新的任务需要执行时,线程池会创建新的线程,直到创建的线程数量达到corePoolSize时,则会将新的任务加入到等待队列中。若等待队列已满,即超过ArrayBlockingQueue初始化的容量,则继续创建线程,直到线程数量达到maximumPoolSize设置的最大线程数量,若大于maximumPoolSize,则执行拒绝策略。在这种情况下,线程数量的上限与有界任务队列的状态有直接关系,如果有界队列初始容量较大或者没有达到超负荷的状态,线程数将一直维持在corePoolSize以下,反之当任务队列已满时,则会以maximumPoolSize为最大线程数上限。

 

 

无界的任务队列

无界任务队列可以使用LinkedBlockingQueue实现

 

ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());

 

使用无界任务队列,线程池的任务队列可以无限制的添加新的任务,而线程池创建的最大线程数量就是你corePoolSize设置的数量,也就是说在这种情况下maximumPoolSize这个参数是无效的,哪怕你的任务队列中缓存了很多未执行的任务,当线程池的线程数达到corePoolSize后,就不会再增加了;若后续有新的任务加入,则直接进入队列等待,当使用这种任务队列模式时,一定要注意你任务提交与处理之间的协调与控制,不然会出现队列中的任务由于无法及时处理导致一直增长,直到最后资源耗尽的问题。

 

 

 

优先任务队列

优先任务队列通过PriorityBlockingQueue实现

 

ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, new PriorityBlockingQueue<Runnable>(),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());

 

除了第一个任务直接创建线程执行外,其他的任务都被放入了优先任务队列,按优先级进行了重新排列执行,且线程池的线程数一直为corePoolSize,也就是只有一个。PriorityBlockingQueue它其实是一个特殊的无界队列,它其中无论添加了多少个任务,线程池创建的线程数也不会超过corePoolSize的数量,只不过其他队列一般是按照先进先出的规则处理任务,而PriorityBlockingQueue队列可以自定义规则根据任务的优先级顺序先后执行。

 

什么是阻塞队列和非阻塞队列

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法。

支持阻塞的插入方法offer:当队列满时,队列会阻塞插入元素的线程,直到队列不满。

支持阻塞的移除方法take:在队列为空时,获取元素的线程会等待队列变为非空。

阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器。

非阻塞队列:非阻塞队列也就是普通队列,它的名字中不会包含 BlockingQueue 关键字,并且它不会包含 put 和 take 方法,若队列为空从中获取元素则会返回空,若队列满了插入元素则会抛出异常。

 

 

 

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

  1. 因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换。阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。使得在线程不至于一直占用cpu资源。
  2. 创建线程池的消耗较高。 (线程池创建线程需要获取mainlock这个全局锁,影响并发效率,阻塞队列可以很好的缓冲。)

 

拒绝策略

一般我们创建线程池时,为防止资源被耗尽,任务队列都会选择创建有界任务队列,但种模式下如果出现任务队列已满且线程池创建的线程数达到你设置的最大线程数时,这时就需要你指定ThreadPoolExecutor的RejectedExecutionHandler参数即合理的拒绝策略,来处理线程池"超载"的情况。ThreadPoolExecutor自带的拒绝策略如下:

  1. AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作;
  2. CallerRunsPolicy策略:如果线程池的线程数量达到上限,该策略会把任务队列中的任务放在调用者线程当中运行;
  3. DiscardOledestPolicy策略:该策略会丢弃任务队列中最老的一个任务,也就是当前任务队列中最先被添加进去的,马上要被执行的那个任务,并尝试再次提交;
  4. DiscardPolicy策略:该策略会默默丢弃无法处理的任务,不予任何处理。当然使用此策略,业务场景中需允许任务的丢失;

 

以上内置的策略均实现了RejectedExecutionHandler接口,当然你也可以自己扩展RejectedExecutionHandler接口,定义自己的拒绝策略

 

 

监控线程池运行状态

一般通过对beforeExecute()、afterExecute()和terminated()的实现来监控线程池的运行状态

  1. beforeExecute:线程池中任务运行前执行
  2. afterExecute:线程池中任务运行完毕后执行
  3. terminated:线程池退出后执行

 

 

Spring中的线程池

Spring中默认线程池是simpleAsyncTaskExecutor,但 Spring 更加推荐我们使用 ThreadPoolTaskExecutor 类来创建线程池,其本质是对ThreadPoolExecutor 的包装。

Spring默认线程池simpleAsyncTaskExecutor

 Spring异步线程池的接口类是TaskExecutor,本质还是java.util.concurrent.Executor,没有配置的情况下,默认使用的是simpleAsyncTaskExecutor。

simpleAsyncTaskExecutor的特点是,每次执行任务时,它会重新启动一个新的线程,并允许开发者控制并发线程的最大数量(concurrencyLimit),从而起到一定的资源节流作用。默认是concurrencyLimit取值为-1,即不启用资源节流。

 

Spring推荐线程池ThreadPoolTaskExecutor

配置线程池参数,可以通过properties配置文件或者yaml

 


# 核心线程池数
spring.task.execution.pool.core-size=5

# 最大线程池数
spring.task.execution.pool.max-size=10

# 任务队列的容量
spring.task.execution.pool.queue-capacity=5

# 非核心线程的存活时间
spring.task.execution.pool.keep-alive=60

 

在编写线程池的config类时,通过@Value注解读取配置文件中的属性

@Configuration
public class AsyncScheduledTaskConfig {
    @Value("${spring.task.execution.pool.core-size}")
    private int corePoolSize;
    @Value("${spring.task.execution.pool.max-size}")
    private int maxPoolSize;
    @Value("${spring.task.execution.pool.queue-capacity}")
    private int queueCapacity;
    @Value("${spring.task.execution.pool.keep-alive}")
    private int keepAliveSeconds;
    
    @Bean
    public Executor myAsync() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //最大线程数
        executor.setMaxPoolSize(maxPoolSize);
        //核心线程数
        executor.setCorePoolSize(corePoolSize);
        //任务队列的大小
        executor.setQueueCapacity(queueCapacity);
        //线程存活时间
        executor.setKeepAliveSeconds(keepAliveSeconds);

        /**
         * 拒绝处理策略
         * CallerRunsPolicy():交由调用方线程运行,比如 main 线程。
         * AbortPolicy():直接抛出异常。
         * DiscardPolicy():直接丢弃。
         * DiscardOldestPolicy():丢弃队列中最老的任务。
         */
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        //线程初始化
        executor.initialize();
        return executor;
    }
}

在方法上添加@Async注解,然后还需要在@SpringBootApplication启动类或者@Configuration注解类上 添加注解@EnableAsync启动多线程注解,@Async就会对标注的方法开启异步多线程调用,注意,这个方法的类一定要交给Spring容器来管理

 

拒绝策略

rejectedExectutionHandler参数字段用于配置绝策略,常用拒绝策略如下

  • AbortPolicy:用于被拒绝任务的处理程序,它将抛出RejectedExecutionException
  • CallerRunsPolicy:用于被拒绝任务的处理程序,它直接在execute方法的调用线程中运行被拒绝的任务。
  • DiscardOldestPolicy:用于被拒绝任务的处理程序,它放弃最旧的未处理请求,然后重试execute。
  • DiscardPolicy:用于被拒绝任务的处理程序,默认情况下它将丢弃被拒绝的任务。

 

 

线程池处理流程

线程池处理流程

  1. 向线程池中添加任务command,线程池通过execute提交任务 
  2. 如果当前线程池中核心线程没有达到设定值corePoolSize,则调用addWorker(command, true)方法创建核心线程执行任务
  3. 如果当前线程池中核心线程数已达到设定值corePoolSize,则判断任务队列是否已满
    1. 如果任务队列没有满则添加任务到等待队列中去,等待执行
    2. 如果任务队列已满,则判断当前线程池中的最大线程数是否达到设定值maximumPoolSize
      1. 如果没有达到设定的最大线程数,那么会调用addWorker(command, false)方法创建一个非核心线程执行任务
      2. 如果达到最大线程数,那么会执行拒绝策略

 

 

线程池状态转换

线程池状态转换

  1. RUNNING:运行状态,状态码为-1;线程池创建好之后就会进入此状态,如果不手动调用关闭方法,那么线程池在整个程序运行期间都是此状态。
  2. SHUTDOWN:关闭状态,状态码为0;不再接受新任务提交,但是会将已保存在任务队列中的任务处理完。在RUNNING状态时调用shutdown()方法来转换到SHUTDOWN状态
  3. STOP:停止状态,状态码为1;不再接受新任务提交,并且会中断当前正在执行的任务、放弃任务队列中已有的任务。在RUNNING状态时调用shutdownNow()方法来转换到STOP状态
  4. TIDYING:整理状态,状态码为2;所有的任务都执行完毕后(也包括任务队列中的任务执行完),当前线程池中的活动线程数降为 0 时的状态。到此状态之后,会调用线程池的 terminated() 方法。
  5. TERMINATED:销毁状态,状态码为3;当执行完线程池的 terminated() 方法之后就会变为此状态。

 

  1. 当调用 shutdown() 方法时,线程池的状态会从 RUNNING 到 SHUTDOWN,当执行的任务为空且阻塞队列中的任务也为空时,线程池从SHUTDOWN转变为TIDYING,最后到 TERMENATED 销毁状态。
  2. 当调用 shutdownNow() 方法时,线程池的状态会从 RUNNING 到 STOP,当执行的任务为空时,线程池从STOP转变为TIDYING,最后到 TERMENATED 销毁状态。

  P.S.terminated() 钩子方法:源码中默认是空的,因此我们在创建线程池的时候可以重写terminated方法,来监控线程池的运行状态

如何配置线程池

主要看任务类型,一般任务分为三种:CPU密集型任务IO密集型任务以及混合型任务

  1. CPU密集型任务: 尽量使用较小的线程池,一般为CPU核心数+1。
  2. IO密集型任务: 可以使用稍大的线程池,一般为2*CPU核心数。
  3. 混合型任务: 可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。

 

posted @ 2023-03-08 02:42  destiny-2015  阅读(63)  评论(0编辑  收藏  举报