java并发专题之线程池深入浅出

240 阅读6分钟

1.线程池是什么

线程池(Thread Pool)是一种基于池化思想管理线程的工具,经常出现在多线程服务器中,如MySQL。

我们都知道线程的创建和销毁都需要一定的资源开销,降低了计算机的整体性能。那么有没有一种办法能避免频繁的线程创建和销毁呢?基于此就引出了线程池的概念,使用线程池可以带来一系列好处:

  • 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
  • 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
  • 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

2.线程池的设计

Java中JDK8的线程池核心实现类是ThreadPoolExecutor,我们首先来看一下ThreadPoolExecutor的UML类图,了解下ThreadPoolExecutor的继承关系。

image-20210807111906325.png

ThreadPoolExecutor实现的顶层接口是Executor,顶层接口Executor提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程来执行任务,用户只需提供Runnable对象,将任务的运行逻辑提交到执行器(Executor)中,由Executor框架完成线程的调配和任务的执行部分。ExecutorService接口增加了一些能力:(1)扩充执行任务的能力,补充可以为一个或一批异步任务生成Future的方法;(2)提供了管控线程池的方法,比如停止线程池的运行。AbstractExecutorService则是上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。最下层的实现类ThreadPoolExecutor实现最复杂的运行部分,ThreadPoolExecutor将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。

3.线程池的实现

3.1. 通过Executors提供四种线程池:

1、newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收线程,若无可回收,则新建线程;线程池无限大,当执行第二个任务时第一个任务已完成,会复用执行第一个任务的线程,而不是新建线程。

2、newFixedThreadPool创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待;初始线程数和最大线程数一样,如果要执行的线程大于初始线程数,则会将多余的线程任务加入到缓存队列中等待执行。

3、newScheduledThreadPool创建一个定长线程池,支持定时及周期性任务的执行;

4、newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行;

通过Executors创建的线程池有个致命缺点,以newCachedThreadPool来说
public class ThreadPoolDemo {
    public static void main(String[] args)throws Exception {
        ExecutorService threadPool = Executors.newCachedThreadPool();
    }
}

我们点进newCachedThreadPool()方法会看到如下内容:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
//我们发现第二个参数为Integer.MAX_VALUE,也就是最大的工作线程数为Integer.MAX_VALUE,如果超高并发过来会直接OOM。

所以我们得通过new ThreadPoolExecutor()来自己指定参数。

3.2通过ThreadPoolExecutor来创建线程池

构造方法如下:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
  • corePoolSize:表示核心线程的数量,就是线程池中最好要保证的线程数量。
  • maximumPoolSize:最大线程数量,就是当任务很多时,能工作的最大线程数。
  • keepAliveTime:线程的空闲时间。就是当实际工作的线程数小于最大线程数的时候,就有部分线程是处于空闲的状态,当这些空闲线程的空闲时间到达keepAliveTime就会被干掉。但是要保证最少线程数为corePoolSize。
  • unit:空闲时间都单位。
  • workQueue:工作队列,就是当工作线程数达到corePoolSize之后的任务会放入到工作队列中。
  • threadFactory:线程工厂,用于创建线程。
  • handler:拒绝处理器,就是当工作线程到达最大工作线程并且工作队列已经满了情况下,线程池应该怎么做。

3.3线程池的工作流程。

流程图如下:

image-20210807114929296.png

当任务过来时,首先会先去判断线程池中工作的线程数量是否到达核心线程数,如果没达到就直接执行,如果达到了就查看工作队列是否满。如果没满就将任务放入到工作队列中,如果满了就增加工作线程数来处理任务。如果工作线程和队列都满了的话就会用制定的策略去拒绝任务。

3.4 拒绝策略

  • AbortPolicy(默认):直接抛出异常RejectedExecutionException异常阻止系统正常运行。

  • CallerRunsPolicy:调用者运行是一种调节机制,该策略既不会抛弃任务,也不会抛弃异常,而是将某些任务回退到调用者,从而降低新任务的流量。

  • DiscardOldestPolicy:丢弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。

  • DiscardPolicy:直接丢弃任务,不予处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。

代码演示拒绝策略:

1.AbortPolicy

public static void main(String[] args)throws Exception {
    ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(2,5,1L,TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(3),Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy());
    try {
        for (int i = 0; i < 10; i++) {
            poolExecutor.execute(()->{
                System.out.println(Thread.currentThread().getName()+"\t办理业务");
            });
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        poolExecutor.shutdown();
    }
    System.out.println(Thread.currentThread().getName()+"\t办理业务");
}

image-20210807161417484.png

也就是说明当工作线程和工作队列都满了之后线程池会拒绝任务直接报错。

2.CallerRunsPolicy

public static void main(String[] args)throws Exception {
    ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(2,5,1L,TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(3),Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.CallerRunsPolicy());
    try {
        for (int i = 0; i < 10; i++) {
            poolExecutor.execute(()->{
                System.out.println(Thread.currentThread().getName()+"\t办理业务");
            });
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        poolExecutor.shutdown();
    }
    System.out.println(Thread.currentThread().getName()+"\t办理业务");
}

image-20210807161606775.png

也就是说明当工作线程和工作队列都满了之后会将任务返还给调用线程池的人,让他去处理。

3.DiscardOldestPolicy和DiscardPolicy

public static void main(String[] args)throws Exception {
    ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(2,5,1L,TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(3),Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.DiscardOldestPolicy());
    try {
        for (int i = 0; i < 10; i++) {
            poolExecutor.execute(()->{
                System.out.println(Thread.currentThread().getName()+"\t办理业务");
            });
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        poolExecutor.shutdown();
    }
    System.out.println(Thread.currentThread().getName()+"\t办理业务");
}

image-20210807161728902.png

一共输出了10条记录,说明有一条消息被抛弃了。