Java多线程专题-线程池原理

本文详细介绍了线程池的概念、工作原理及Java中线程池的实现方式,包括四种常见线程池类型及其应用场景,同时探讨了如何合理配置线程池以提升程序性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

线程池概念及应用

线程池是为了省去频繁创建线程所带来的开销,统一管理线程,在程序一开始就创建一些线程供程序使用,使用完毕后不会结束该线程,而是放回线程池,以便被再次使用,可以理解为线程池是一个线程的集合,每一个线程都可被多次使用。

Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序
都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处。

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  2. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的可管理性。

线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。

线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少了创建和销毁线程所需的时间,从而提高效率。

如果一个线程的时间非常长,就没必要用线程池了(不是不能作长时间操作,而是不宜。),况且我们还不能控制线程池中线程的开始、挂起、和中止。

线程池原理

线程池的创建源码分析

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }



    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.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
  • corePoolSize:线程池核心线程数量,即初始线程数
  • maximumPoolSize:线程池最大线程数量
  • keepAliverTime:当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间
  • unit:存活时间的单位
  • workQueue:存放任务的队列
  • handler:超出线程范围和队列容量的任务的处理程序

线程池中任务的执行流程

在创建线程池的过程中,最终会调用以上构造方法去创建线程,可根据不同的线程池策略使用参数进行调整。在创建完成后,将一些线程任务加入到线程池中后,线程池的处理流程如下:

  • 判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
  • 线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
  • 判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。

https://i-blog.csdnimg.cn/blog_migrate/d8d58eb698b056a3679a001e34ad882e.jpeg

工作队列中的任务在什么时候被执行

先查看以下源码:

   while (task != null || (task = getTask()) != null) {
       task.run();      
   }

摘选自java.util.concurrent.ThreadPoolExecutor.runWorker(Worker w);方法。代码片段,其中getTask()是从任务队列(workQueue)中获取一个任务,有兴趣的话可以查看JDK完成源码。

由此可以看出,线程池只要有线程执行结束,就会从队列中继续取出任务执行,所以队列中的任务只要线程池有线程空闲,就会被执行。

线程池的分类

Executor框架的最顶层实现是ThreadPoolExecutor类,Executors工厂类中提供的newFixedThreadPool、newCachedThreadPool等方法其实也只是ThreadPoolExecutor的构造函数参数不同而已。

Java通过Executors(jdk1.5并发包)提供四种线程池,分别为:

  • newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
  • newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行。

newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。经查看源码如下:

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

在我们调用ExecutorService threadPool = Executors.newCachedThreadPool();获取一个新的线程池时,实际上又调用了ThreadPoolExecutor的构造器,返回的是一个ThreadPoolExecutor对象。从参数上看,可缓存线程池原理上是;

  • 核心池大小也就是初始线程数为0;
  • 最大线程数为Integer.MAX_VALUE,可理解为不限制大小;
  • 每个线程可存活60秒,即无任务执行状态下,60秒即被销毁;
  • 使用SynchronousQueue作为工作队列,即不保存元素,直接使用最大可用线程数创建线程。

newFixedThreadPool

创建一个固定大小的线程池,可根据参数控制核心线程数和最大线程数:

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

可使用ExecutorService threadPool = Executors.newThreadPoolExecutor (4);创建一个固定长度的线程池,以经验来看,可根据系统资源来控制线程池的大小,一般使用机器的处理器核心数,可通过Runtime.getRuntime().availableProcessors()获取处理器核心数。

由源码可以看出,固定长度的线程池的创建原理为:

  • 通过参数来控制了核心线程数和最大线程数,即核心线程数等于最大线程数,当核心线程数已满后,新任务只能存储在并发队列中。
  • 每个线程在无任务的状态下,存活时间为0,即立即销毁。

并发队列使用LinkedBlockingQueue,并且未指定队列长度,即不限制长度。

newScheduledThreadPool

创建一个定长可调度线程池,支持定时及周期性任务执行

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


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


    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler);
    }



    static class DelayedWorkQueue extends AbstractQueue<Runnable>
        implements BlockingQueue<Runnable> {…}

可以看出,newScheduledThreadPool方法返回值与其他的不同,是ExecutorService的子接口ScheduledExecutorService,可以使用ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(4)创建一个可调度的线程池,通过以上源码,最终依然是调用ThreadPoolExecutor的构造器去创建线程池,只不过使用了自定义的一个工作队列,这个工作队列可调度线程执行任务。

由以上四组源码可以看出,可调度的线程池实现原理是:

  • 通过参数指定核心线程数
  • 不限制最大线程数
  • 当线程无任务时,立即销毁

使用DelayedWorkQueue作为工作队列。

    public ScheduledFuture<?> schedule(Runnable command,
                                       long delay, TimeUnit unit);


    public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                                           long delay, TimeUnit unit);


    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit);


    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit);

newSingleThreadExecutor

创建一个单一线程的线程池,所有任务按顺序依次执行。

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

通过 ExecutorService threadPool = Executors.newSingleThreadExecutor()创建单一线程的线程池,核心线程数和最大线程数都为1,说明线程池中只能有一个线程,多余的任务存储在LinkedBlockingQueue中,但核心线程执行完后,依次执行。

饱和策略

当队列和线程池都满了,说明线程池处于饱和状态,那么必须对新提交的任务采用一种特殊的策略来进行处理。这个策略默认配置是AbortPolicy,表示无法处理新的任务而抛出异常。JAVA提供了4中策略:

  1. AbortPolicy:直接抛出异常
  2. CallerRunsPolicy:只用调用所在的线程运行任务
  3. DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
  4. DiscardPolicy:不处理,丢弃掉。

在以上介绍的线程池中,CachedThreadPool和ScheduledThreadPool都不限制最大线程数,FixedThreadPool和SingleThreadExecutor都不限制队列大小,所以并不会出现饱和的情况。只有在自定义线程池中才有可能使用到饱和策略。

以第四种饱和策略为例,使用如何方案定义饱和策略,并将返回值的handler作为参数传入ThreadPoolExecutor的构造器中即可。

RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardPolicy();

合理配置线程池

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

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

任务性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池。IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。

优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。

执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。

依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值