聊聊并发编程——线程池

目录

Java线程池

处理流程

线程池主要参数

常见的拒绝策略

execute和submit区别

关闭线程池

常见的线程池

newSingleThreadExecutor

newFixedThreadPool

newCachedThreadPool

newScheduledThreadPool

线程池的状态


Java线程池

运用场景最多的并发框架,几乎所有需要异步或者并发执行任务的程序都可以使用线程池。如下好处:

  • 降低资源消耗。重复利用已创建的线程降低线程创建和销毁造成的消耗。

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

  • 提高线程的客观理性。使用线程池可以进行统一分配、调优和监控。

处理流程

当新任务到达线程池的时候,处理流程如下:

  1. 判断核心线程池里的线程是否都在执行任务:

    • 否,创建新的工作线程执行任务。

    • 是,去看阻塞队列。

  2. 判断阻塞队列是否满了:

    • 否,将新任务存储在阻塞队列中。

    • 是,去看整个线程池。

  3. 判断线程池的线程是否都处于工作状态:

    • 否,创建新的工作线程执行任务。

    • 是,去看拒绝策略吧。

  4. 根据拒绝策略进行处理。

聊聊并发编程——线程池_第1张图片

ThreadPoolExecutor执行execute方法分为下面4中情况:

  1. 当前运行的线程少于corePoolSize,创建新线程执行任务。(需获取全局锁)

  2. 运行的线程等于或多余corePoolSize,则将任务加入BlockingQueue。

  3. 如果队列已满,且运行线程数少于maximumPoolSize,创建新线程执行任务。(需获取全局锁)

  4. 如果创建新线程将使得当前运行的线程超过maximumPoolSize,任务被拒绝,执行RejectedExecutionHandler.rejectedExecution()方法。

聊聊并发编程——线程池_第2张图片

网上有看到一个生动的例子,可能会让大家印象更深刻些:

银行的营业大厅有6个业务窗口,现在开放了3个,有3个业务员负责办理业务。这天,二柱去办理业务,他可能会遇到什么情况呢?

  1. 3个业务窗口正好空闲,他可以直接去办理业务。

  2. 3个业务窗口都有人,所以他得去排队。

  3. 3个业务窗口都有人而且队也排满了。这是经理赶紧开放了另外3个窗口,排队的可以先去办理。

  4. 6个业务窗口都有人了,队伍也排满了。二柱子去找经理,经理说明天再来或者先不办了。

线程池主要参数
  1. 核心线程数(corePoolSize): 核心线程数是线程池中保持活动状态的最小线程数量。即使线程池中没有任务需要执行,核心线程也会保持活动状态,不会被销毁。核心线程数通常用来处理短期生存期的任务,以减少线程的创建和销毁开销。

  2. 最大线程数(maximumPoolSize): 最大线程数是线程池中允许存在的最大线程数量。当任务数量超过核心线程数并且任务队列已满时,线程池会创建新的线程,但不会超过最大线程数。最大线程数可以控制线程池的最大并发性。

  3. 任务队列(BlockingQueue): 任务队列用于存储等待执行的任务。当线程池中的线程数达到核心线程数时,新的任务会被放入任务队列中等待执行。任务队列可以是不同类型的队列,如无界队列(如 LinkedBlockingQueue)或有界队列(如 ArrayBlockingQueue)。

  4. 线程存活时间(keepAliveTime): 线程存活时间是在核心线程数之外的线程在没有任务可执行时保持活动状态的最长时间。当线程池中的线程数量超过核心线程数,空闲的非核心线程在经过一定时间后会被销毁,以减少资源占用。

  5. 拒绝策略(RejectedExecutionHandler): 拒绝策略定义了当线程池无法接受新的任务时应该采取的动作。常见的拒绝策略包括抛出异常、丢弃任务、丢弃最旧的任务、调用提交任务的线程来执行任务等。

  6. 线程工厂(ThreadFactory): 线程工厂用于创建线程池中的线程。通过自定义线程工厂,可以为线程池中的线程设置自定义的名称、优先级、守护状态等属性。

常见的拒绝策略
  1. AbortPolicy(默认策略): 这是默认的拒绝策略,它会抛出一个 RejectedExecutionException 异常,告诉调用者线程池已满,无法处理新任务。这是最常用的拒绝策略,它会防止任务堆积,但可能会导致任务丢失。

    ThreadPoolExecutor executor = new ThreadPoolExecutor(
        corePoolSize, 
        maximumPoolSize, 
        keepAliveTime, 
        TimeUnit.SECONDS, 
        workQueue,
        new ThreadPoolExecutor.AbortPolicy());
  2. CallerRunsPolicy: 这个策略不会抛出异常,而是将任务回退到调用者,让调用者来执行。这可以用于保证任务的执行,但可能会导致调用者的线程阻塞。

    ThreadPoolExecutor executor = new ThreadPoolExecutor(
        corePoolSize, 
        maximumPoolSize, 
        keepAliveTime, 
        TimeUnit.SECONDS, 
        workQueue,
        new ThreadPoolExecutor.CallerRunsPolicy());
  3. DiscardPolicy: 这个策略会默默地丢弃掉无法处理的新任务,不会抛出异常。这可能会导致任务丢失,但对于不太重要的任务可以使用。

    ThreadPoolExecutor executor = new ThreadPoolExecutor(
        corePoolSize, 
        maximumPoolSize, 
        keepAliveTime, 
        TimeUnit.SECONDS, 
        workQueue,
        new ThreadPoolExecutor.DiscardPolicy());
  4. DiscardOldestPolicy: 这个策略会丢弃队列中最旧的任务,然后尝试重新提交新任务。它可能会导致一些任务被丢弃,但对于有优先级的任务可能是一个不错的选择。

    ThreadPoolExecutor executor = new ThreadPoolExecutor(
        corePoolSize, 
        maximumPoolSize, 
        keepAliveTime, 
        TimeUnit.SECONDS, 
        workQueue,
        new ThreadPoolExecutor.DiscardOldestPolicy());
  5. 自定义策略: 你也可以自定义拒绝策略,只需实现 RejectedExecutionHandler 接口的 rejectedExecution 方法,然后将其传递给线程池的构造函数。

    class CustomRejectPolicy implements RejectedExecutionHandler {
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            // 自定义拒绝策略的逻辑
        }
    }
    ​
    ThreadPoolExecutor executor = new ThreadPoolExecutor(
        corePoolSize, 
        maximumPoolSize, 
        keepAliveTime, 
        TimeUnit.SECONDS, 
        workQueue,
        new CustomRejectPolicy());

选择适当的拒绝策略取决于应用程序的需求。通常,如果任务的重要性很高,你可能会选择将任务回退到调用者或者使用自定义策略来处理。如果任务不太重要,你可以使用默认的 AbortPolicyDiscardPolicy 策略。需要根据具体情况权衡任务的执行和资源利用之间的权衡。

execute和submit区别

可以使用两个方法向线程池提交任务,分别为execute()和submit()方法。

execute()方法用于提交不需要返回值的任务。

 threadsPool.execute(new Runnable() { 
     @Override 
     public void run() { 
         // TODO Auto-generated method stub 
     } 
 });

submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个 future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方 法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线 程一段时间后立即返回,这时候有可能任务没有执行完。

Future future = executor.submit(harReturnValuetask); 
    try { 
        Object s = future.get(); 
    } catch (InterruptedException e) { 
        // 处理中断异常 
    } catch (ExecutionException e) { 
        // 处理无法执行任务异常 
    } finally { 
        // 关闭线程池 
        executor.shutdown(); 
    }
关闭线程池

可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。

shutdown() 将线程池状态置为shutdown,并不会⽴即停⽌:

  1. 停⽌接收外部submit的任务

  2. 内部正在跑的任务和队列⾥等待的任务,会执⾏完

  3. 等到第⼆步完成后,才真正停⽌

shutdownNow() 将线程池状态置为stop。⼀般会⽴即停⽌,事实上不⼀定:

  1. 和shutdown()⼀样,先停⽌接收外部提交的任务

  2. 忽略队列⾥等待的任务

  3. 尝试将正在跑的任务interrupt中断

  4. 返回未执⾏的任务列表

shutdown 和shutdownnow简单来说区别如下:

  • shutdownNow()能⽴即停⽌线程池,正在跑的和正在等待的任务都停下了。这样做⽴即⽣效,但是⻛险也⽐ 较⼤。

  • shutdown()只是关闭了提交通道,⽤submit()是⽆效的;⽽内部的任务该怎么跑还是怎么跑,跑完再彻底停 ⽌线程池。

常见的线程池

常见的有四种,都是通过Executors 工具类创建的,但是不建议。Executors各个方法的弊端:newFixedThreadPool和newSingleThreadExecutor: 主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。

newCachedThreadPool和newScheduledThreadPool: 主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

聊聊并发编程——线程池_第3张图片

newSingleThreadExecutor

直接调用ThreadPoolExecutor的构造⽅法。

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

线程池特点:

  • 核心线程数1

  • 最大线程数1

  • 阻塞队列是无界队列LinkedBlockingQueue,可能会导致OOM

  • keepAliveTime为0

使用场景:串行执行任务的场景,一个任务一个任务地执行

newFixedThreadPool

直接调用ThreadPoolExecutor的构造⽅法。

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

线程池特点:

  • 核心线程数和最大线程数大小一样

  • keepAliveTime为0

  • 阻塞队列是无界队列LinkedBlockingQueue,可能会导致OOM

使用场景:适用于执行长期的任务

newCachedThreadPool

直接调用ThreadPoolExecutor的构造⽅法。

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

线程池特点:

  • 核心线程数0

  • 最大线程数Integer.MAX_VALUE,可能无限创建线程,导致OOM

  • 阻塞队列是SynchronousQueue

  • 非核心线程空闲存活时间为60s

使用场景:并发执行大量短期的小任务

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

线程池特点:

  • 最大线程数Integer.MAX_VALUE,可能无限创建线程,导致OOM

  • 阻塞队列DelayedWorkQueue

  • keepAliveTime为0

  • scheduleAtFixedRate() :按某种速率周期执⾏

  • scheduleWithFixedDelay():在某个延迟后执⾏

使用场景:周期性执行任务的场景,需要限制线程数量的场景

线程池的状态
  1. RUNNING(运行中): 线程池处于运行状态,可以接受新的任务,并且正在执行任务队列中的任务。

  2. SHUTDOWN(关闭中): 线程池进入关闭状态,不再接受新的任务提交,但会继续执行已经提交的任务,直到所有任务都完成。

  3. STOP(停止中): 线程池进入停止状态,不再接受新的任务提交,并且尝试中断正在执行的任务。这个状态会尽量终止线程池的执行。

  4. TIDYING(整理中): 线程池正在执行清理操作。当线程池的任务都完成后,它会进入这个状态,执行一些必要的清理操作,例如关闭线程。

  5. TERMINATED(已终止): 线程池已经完全终止,不再有任何活动线程。线程池的状态将永远停留在这个状态。

线程池的状态通常通过调用线程池的方法来进行转换,例如 shutdown() 方法将线程池从 RUNNING 状态转换为 SHUTDOWN 状态,shutdownNow() 方法将线程池从 RUNNING 状态转换为 STOP 状态。一旦线程池进入 TERMINATED 状态,就无法再切换到其他状态。

你可能感兴趣的:(并发编程,java,jvm,算法)