Java多线程——线程池的基本使用

本文详细探讨了线程池的概念,为何需要线程池,以及Java中Executor框架的使用。重点介绍了ThreadPoolExecutor的核心参数和ScheduledThreadPoolExecutor的周期性任务执行策略。

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

线程池

本篇基于JDK1.8。

一 为什么需要线程池?
  • 直接创建线程的缺点:
    1. 每次通过new Thread()创建对象性能不佳。
    2. 可能会无限制的创建新的线程,造成系统资源匮乏,严重可能导致OOM。
    3. 缺乏管理性,没有一个统一的东西去管理线程的生命周期。
  • 使用线程池的好处:
    1. 可重用存在的空闲线程,减少线程的多次创建,提升性能。
    2. 可设置其线程的最大创建数量和核心线程的数量,避免无限制的创建。
    3. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
    4. 可管理线程的生命周期,需要时创建,在空闲等待事件到达之后销毁线程。
    5. 扩展线程的功能,可定时执行,单线执行或者并发执行任务。
二 Executor

Java 1.5中提供了Executor框架用于把任务的提交和执行解耦,任务的提交,交给了Runable或者Callable,而Executor框架用来处理任务。该框架包括三个重要的部分:

  1. 任务。也就是工作的单元,Runnable、Callable及其子类。
  2. 任务的执行。包括任务执行机制的核心接口Executor,以及继承自ExecutorExecutorService接口。Executor框架有两个关键类实现了ExecutorService接口(ThreadPoolExecutorScheduledThreadPoolExecutor)。
  3. 异步计算的结果。包括Future接口及实现了Future接口的FutureTask类。

Excutor是一个接口类型,其子接口为ExecutorService,核心实现类是ThreadPoolExecutorThreadPoolExecutor的子类是ScheduledThreadPoolExecutor,它可以延迟或者是定期执行任务。继承关系如图:
网络选图,违权必删

ThreadPoolExecutor

ThreadPoolExecutor则是Executor框架的核心实现类。它的构造方法中,有很多很重要的参数。参数将决定该线程池的主要用途:

//ThreadPoolExecutor.java
 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
    //。。。省略代码。
}
  • 参数corePoolSize

    核心线程池的大小。也就是就算是处于空闲状态,线程也不会被回收,如果设置了allowCoreThreadTimeOut = true核心线程还是会被回收。如果调用了prestartAllCoreThreads()方法,那么线程池会提前创建并启动所有基本线程。

    //ThreadPoolExecutor.java
    //启动所有核心线程,导致它们空闲地等待工作。这将覆盖仅在执行新任务时启动核心线程的默认策略。
    public int prestartAllCoreThreads() {
            int n = 0;
            while (addWorker(null, true))
                ++n;
            return n;
        }
    

    SingleThreadExecutor类型的线程池中,核心线程数默认为1。

  • 参数maximumPoolSize

    线程池中的最大线程数。线程池在执行任务的时候,所允许的最大并发数量。一般设置为Integer.MAX_VALUE

  • 参数keepAliveTime

    线程空闲后等待的时间,等待时间结束之后会回收线程。OkHttp中设置线程的空闲时间是60s。

  • 参数unit

    线程空闲后等待的事件单位。

  • 参数workQueue

    阻塞队列。在执行任务之前用于保存任务的队列。此队列将仅包含由execute方法提交的Runnable任务。常用的有ArrayBlockingQueueLinkedBlockingQueueSynchronousQueuePriorityBlockingQueue

  1. CachedThreadPool类型的线程池中采用的是SynchronousQueue

  2. SingleThreadExecutor类型的线程池中采用的是LinkedBlockingQueue

  3. FixedThreadPool类型的线程池中采用的是LinkedBlockingQueue

  4. ScheduledThreadPool类型的线程池中采用的是DelayedWorkQueue

  • 参数threadFactory

    线程池中创建Thread的工厂,可以自行实现。在实现的时候可以给线程命名。按照阿里的规范,创建的线程池都要实现自己的ThreadFactory

    Executors工厂类中的默认ThreadFactory

    //Executors.java 
    private static class DefaultThreadFactory implements ThreadFactory {
            //。。。省略代码。
            DefaultThreadFactory() {
               //。。。省略代码。
            }
            public Thread newThread(Runnable r) {
                Thread t = new Thread(group, r,namePrefix + threadNumber.getAndIncrement(),0);
                if (t.isDaemon())
                    t.setDaemon(false);
                if (t.getPriority() != Thread.NORM_PRIORITY)
                    t.setPriority(Thread.NORM_PRIORITY);
                return t;
            }
        }
    
  • 参数handler

    由于达到线程边界和队列容量而阻止执行时要使用的处理程序。也就是饱和策略,当线程池满了的时候,任务无法得到处理,这时候需要饱和策略来处理无法完成的任务。在ThradPoolExecutor类中有一下几种策略,默认的是AbortPolicy

    • AbortPolicy:直接抛异常,如果当前没有线程能执行新任务的时候。
    • CallerRunsPolicy:使用调用者的线程执行当前被拒绝的任务。这里要看实际的用途了。
    • DiscardPolicy:放弃这个被拒绝的任务。一般的最好不是这样的。
    • DiscardOldestPolicy:该处理程序丢弃最旧的未处理请求,然后重试execute,除非该执行程序关闭,否则该任务将被丢弃。
3.1 newScheduledThreadPool

用途: 可以实现延迟运行或者是定期执行的线程池。

概念: ScheduledThreadPoolExecutor线程池比较特殊,是ThreadPoolExecutor的子类,实现了ScheduledExecutorService接口,该接口可以安排任务在给定的时间之后运行,或定期执行。

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

再其构造方法中必须设置核心线程数,最大的线程数是Integer.MAX_VALUE。其阻塞队列为DelayedWorkQueue,是其内部类,采用堆结构是一种优先队列,也就是后进先出。

延迟执行任务:

 ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(3);
 executor.schedule(new Runnable() {
      @Override
      public void run() {
                
      }
 }, 1, TimeUnit.SECONDS);

周期性执行任务:

ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(3);
executor.scheduleAtFixedRate(new Runnable() {
     @Override
     public void run() {
          Log.e("WANG","FlashScreenPageActivity.run");
     }
},1,5,TimeUnit.SECONDS);

当想要延迟执行某个任务的时候,调用的是schedule()方法:

//ScheduledThreadPoolExecutor.java
public ScheduledFuture<?> schedule(Runnable command,long delay,TimeUnit unit) {
        //。。。省略代码。
        return t;
}

 public <V> ScheduledFuture<V> schedule(Callable<V> callable,long delay,TimeUnit unit) {
        //。。。省略代码。
        return t;
    }

//参数1:任务类型。要么执行Runnable对象,要么执行Callable对象。Callable对象是有返回值的。
//参数2:延迟时间。
//参数3:延迟时间的单位。
//返回值:ScheduledFuture对象,里面有个isPeriodic()方法,返回true 表示是周期性的操作,false 表示是一次性的操作。

当要执行周期性的操作时调用的是scheduleAtFixedRate()方法:

//ScheduledThreadPoolExecutor.java
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,
                                              long period,TimeUnit unit){
    //。。。省略代码。
    return t;
}
//参数1:可执行对象。
//参数2:第一次任务执行时候的延迟时间。
//参数3:任务周期的间隔时间,从第一个任务的开始算起。
//参数4:时间单位,也是周期的时间单位。

 public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,
                                                  long delay,TimeUnit unit) {
       //。。。省略代码。
        return t;
    }
//参数1:可执行对象。
//参数2:第一次任务执行时候的延迟时间。
//参数3:第一次任务结束到第二次任务开始时的延迟时间。
//参数4:时间单位,也是周期的时间单位。

ScheduledThreadPoolExecutor在执行周期行任务的时候有两种类型的策略:

  1. 调用scheduleAtFixedRate()方法,该方法的周期计时是在一个任务开始之后。也就是在任务开始之后,就开始记录时间,当时间到达设置的period的时候,判断前一个任务是否已经执行结束,如果执行结束了那就开启下一个任务执行,如果没有执行结束那就一直等到上一个任务执行结束。会出现上个任务的开始和下一个任务的开始之间的间隔大于period值。这主要取决于每个任务的耗时情况。直白一点就是倒计时period值包括了任务执行期间的耗时。
  2. 调用scheduleWithFixedDelay()方法,该方法的延迟计时是在一个任务执行结束之后。也就是在任务执行完毕之后才开始计时,当计时时间达到delay值之后,才会开启下一个任务。不用判断前一个任务的执行情况,因为是在前一个任务结束之前才倒计时。

我们看到两种周期性执行任务的用法不太一样,在做网络轮询的时候最好采用scheduleWithFixedDelay()方法。

ThreadPoolExecutor参数: ScheduledThreadPoolExecutorThreadPoolExecutor的子类,所以在创建的时候,还是通过其后者的构造方法。核心线程数需要自行设置,最大线程数为 Integer.MAX_VALUE,空闲线程等待时间是10ms,不设置的话使用默认的饱和策略。

执行: 任务的执行队列是DelayedWorkQueue,是其内部类。是一种优先级的队列,会对其插入的数据做下优先级的处理,保证优先级高的数据先被处理。内部采用的是堆结构。

3.2 newCachedThreadPool

用途: 创建一个线程池,该线程池根据需要创建新线程,但在可用时将重用先前构造的线程,线程的等待时间是60秒,所以如果太长时间没用使用,那么缓存线程池就跟普通的线程池一致。OkHttp使用的就是这种缓存线程池。也就是线程使用之后会存活60s,超过60s就会回收其线程。

ThreadPoolExecutor参数: 核心线程数为0,最大线程数为 Integer.MAX_VALUE,空闲线程等待时间是60s,使用默认的饱和策略。

执行: 使用的是同步阻塞队列,SynchronizedQueue是一个没有数据缓冲的阻塞队列,take() & put()是会阻塞线程,poll()&offer()非阻塞操作,也就是任务的put()需要先等take()操作结束,反过来也一样。所以该队列中始终只会有一个任务。但是并不是说值存储一个任务,该线程池的模式就只能运行一个任务,它的存储和获取是很快的,当有任务经过put()添加进来的时候,就会唤起线程进行take()操作。并不是等待上一个任务结束才去取下一个任务,而是put()一个任务就会去取一个任务,任务也是并发执行的。

3.3 newSingleThreadExecutor

用途: 该线程池用单个工作线程在无边界队列上操作。(但是,请注意,如果此单线程在关闭之前的执行过程中由于失败而终止,则在需要执行后续任务时将替换新线程)任务保证按顺序执行,并且在任何给定时间都不会有多个任务处于活动状态。与其他等价的newFixedThreadPool(1)不同,返回的线程池不可重新配置新的线程。也就是该线程池只有一个核心线程,且线程一直在运行中,等待着任务。如果在执行任务期间,出现异常使线程中断,会继续创建一个新的线程继续执行任务。

ThreadPoolExecutor参数: 核心线程数为1,最大线程数为 1,空闲线程等待时间是0毫秒,使用默认的饱和策略。

执行: 使用的是LinkedBlockingQueue,任务是按照加入的顺序,顺序执行的。如果队列中有多个任务,也是下一个任务等到上一个结束之后才会执行的。不适合做并发处理。

3.4 newFixedThreadPool

用途: 该线程池将创建一定数量的核心线程数,将会一直运行直到显示的调用了shutdown方法。该线程的创建需要传入核心线程数(nThreads变量),该变量决定了核心线程数和总线程数量。

ThreadPoolExecutor参数: 核心线程数等于总线程数等于nThreads变量,空闲线程等待时间是0毫秒,使用默认的饱和策略。

执行: 使用的是LinkedBlockingQueue,任务是按照加入的顺序,顺序执行的。如果队列中有多个任务,也是下一个任务等到上一个结束之后才会执行的。不适合做并发处理。

3.5 线程池的终止:

线程池的终止通过shutdown()shutdownNow()方法,方法定义在ExecutorService接口中,所以以上几种用途的线程池采用的都是同样的方法。

  • shutdown():

    启动有序关闭,在该关闭中执行以前提交的任务,但不接受新任务。如果已关闭,则调用没有其他效果。

  • shutdownNow():

    尝试停止所有正在执行的任务,停止处理等待的任务,并返回等待执行的任务列表。从该方法返回时,这些任务将从任务队列中排出(移除)。


更多Android知识点~
我的优快云
我的Github
我的掘金
我的简书

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值