线程池 是啥?

一、为什么要用线程池

一个线程执行某个任务的时间片可以分成3块,创建线程T1,执行任务T2,销毁线程T3,现实中往往是 T1+T3>T2,也就是真正执行任务花费的时间很短,反而是创建与销毁线程更耗时,也是就是若我们需要多个线程去执行任务时,线程的创建和销毁会占用更多资源。于是有人提出提前创建一堆线程,然后把它们放在一个容器中统一进行管理,需要用的时候就直接拿出来用,用完之后再放回池子里。这样就不会在线程的创建和销毁上浪费时间。

上面的“池子”就是线程池,很明显线程池可以给我们带来很多好处:

  • 低资源消耗,降低了频繁创建线程和销毁线程的开销
  • 提高响应速度
  • 提高线程的可管理性,可以对线程进行一些操作,方便管理线程

二、线程池中的核心参数 

这个池子给我们带来很多好处,但这个池子不是没有边界的,需要一些参数来限制这个池子。

2.1  corePoolSize

核心线程的最大个数,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。

非核心线程:当等待队列满了,如果当前线程数没有超过最大线程数,则会新建线程执行任务,那么核心线程和非核心线程到底有什么区别呢?说出来你可能不信,本质上它们没有什么区别,创建出来的线程也根本没有标识去区分它们是核心还是非核心的,线程池只会去判断已有的线程数(包括核心和非核心)去跟核心线程数和最大线程数比较,来决定下一步的策略。

2.2  maximumPoolSize

线程池最大线程数,它表示在线程池中最多能创建多少个线程。线程数量超过这个值就会抛异常。

2.3  keepAliveTime

表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize。即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是allowCoreThreadTimeOut(true)方法可以使得线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0。

unit是参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:

TimeUnit.DAYS;               //天
TimeUnit.HOURS;             //小时
TimeUnit.MINUTES;           //分钟
TimeUnit.SECONDS;           //秒
TimeUnit.MILLISECONDS;      //毫秒
TimeUnit.MICROSECONDS;      //微妙
TimeUnit.NANOSECONDS;       //纳秒

2.4  workQueue

任务队列,是一个阻塞队列,用来存储等待执行的任务,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:ArrayBlockingQueue;LinkedBlockingQueue;SynchronousQueue。

  1. ArrayBlockingQueue: 这是一个由数组实现的容量固定的有界阻塞队列,,此队列按 FIFO(先进先出)原则对元素进行排序
  2. SynchronousQueue: 没有容量,不能缓存数据;每个put必须等待一个take; offer()的时候如果没有另一个线程在poll()或者take()的话返回false。静态工厂方法Executors.newCachedThreadPool使用了这个队列。
  3. LinkedBlockingQueue: 这是一个由单链表实现的默认无界的阻塞队列。LinkedBlockingQueue提供了一个可选有界的构造函数,而在未指明容量时,容量默认为Integer.MAX_VALUE。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。

2.5  threadFactory

线程工厂,主要用来创建线程。

2.6  handler

表示当拒绝处理任务时的策略,有以下四种取值:

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务 

三、 Executor、ExecutorService、Executors

 

 

 Executor 是一个抽象层面的核心接口,它定义了execute()方法,用来接收一个Runnable接口的对象。

ExecutorService 接口继承了Executor 接口,是Executor 的子接口。ExecutorService 接口对 Executor 接口进行了扩展,提供了返回 Future 对象,终止,关闭线程池等方法。当调用 shutDown 方法时,线程池会停止接受新的任务,但会完成正在 pending 中的任务。Executor接口中execute()方法不返回任何结果,而ExecutorService接口中submit()方法可以通过一个 Future 对象返回运算结果。通过 ExecutorService.submit() 方法返回的 Future 对象,还可以取消任务的执行。Future 提供了 cancel() 方法用来取消执行 pending 中的任务。

Executors 类提供了若干个静态方法,用于生成不同类型的线程池。但线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,下面将介绍。

四、ThreadPoolExecutor

讲完了上面的核心参数就可以看看怎么创建线程池了,ThreadPoolExecutor是线程池的核心类,有4个构造方法可以得到我们需要的线程池:

 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) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             threadFactory, defaultHandler);
}

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

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;
    }

五、线程的任务处理流程

当在execute(Runnable)方法中提交新任务并且少于corePoolSize线程正在运行时,即使其他工作线程处于空闲状态,也会创建一个新线程来处理该请求。 如果有多于corePoolSize但小于maximumPoolSize线程正在运行,则仅当队列已满时才会创建新线程。 通过设置corePoolSize和maximumPoolSize相同,您可以创建一个固定大小的线程池。 通过将maximumPoolSize设置为基本上无界的值,例如Integer.MAX_VALUE,您可以允许池容纳任意数量的并发任务。 通常,核心和最大池大小仅在构建时设置,但也可以使用setCorePoolSize和setMaximumPoolSize进行动态更改。

六、常见的线程池及其使用场景

在 Executors 类里面提供了一些静态工厂,生成一些常用的线程池。

6.1  newFixedThreadPool

创建固定大小的线程池,每提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

6.2  newCachedThreadPool(推荐使用)

创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。

6.3  newSingleThreadExecutor

创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

6.4  newScheduledThreadPool

创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors各个方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor:
  主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
2)newCachedThreadPool和newScheduledThreadPool:
  主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

 七、如何合理配置线程池的大小

线程池究竟设成多大是要看你给线程池处理什么样的任务,任务类型不同,线程池大小的设置方式也是不同的。

任务一般可分为:CPU密集型、IO密集型、混合型,对于不同类型的任务需要分配不同大小的线程池。

  • CPU密集型任务
    尽量使用较小的线程池,一般为CPU核心数+1。
    因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,只能增加上下文切换的次数,因此会带来额外的开销。
  • IO密集型任务
    可以使用稍大的线程池,一般为2*CPU核心数。
    IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候去处理别的任务,充分利用CPU时间。
  • 混合型任务
    可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。
    只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。
    因为如果划分之后两个任务执行时间相差甚远,那么先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。

八、怎么理解无界队列和有界队列

有界队列
就是有固定大小的队列。比如设定了固定大小的 LinkedBlockingQueue,又或者大小为 0,只是在生产者和消费者中做中转用的 SynchronousQueue。
无界队列
指的是没有设置固定大小的队列。这些队列的特点是可以直接入列,直到溢出。当然现实几乎不会有到这么大的容量(超过 Integer.MAX_VALUE),所以从使用者的体验上,就相当于 “无界”。比如没有设定固定大小的 LinkedBlockingQueue。与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况。当有新的任务到来,系统的线程数小于corePoolSize时,则新建线程执行任务。当达到corePoolSize后,就不会继续增加,若后续仍有新的任务加入,而没有空闲的线程资源,则任务直接进入队列等待。若任务创建和处理的速度差异很大,无界队列会保持快速增长,直到耗尽系统内存。当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略。

常见的有界队列为:

  • ArrayBlockingQueue 基于数组实现的阻塞队列
  • LinkedBlockingQueue 其实也是有界队列,但是不设置大小时就时Integer.MAX_VALUE,内部是基于链表实现的
  • SynchronousQueue 比较奇葩,内部容量为零,适用于元素数量少的场景,尤其特别适合做交换数据用

常见的无界队列:

  • ConcurrentLinkedQueue 无锁队列,底层使用CAS操作,通常具有较高吞吐量,但是具有读性能的不确定性,弱一致性——不存在如ArrayList等集合类的并发修改异常,通俗的说就是遍历时修改不会抛异常
  • PriorityBlockingQueue 具有优先级的阻塞队列
  • DelayedQueue 延时队列,使用场景 
  • LinkedTransferQueue 简单的说也是进行线程间数据交换的利器,在SynchronousQueue 中就有所体现,并且并发大神 Doug Lea 对其进行了极致的优化,使用15个对象填充,加上本身4字节,总共64字节就可以避免缓存行中的伪共享问题,其实现细节较为复杂,可以说一下大致过程: 

九、线程池是有哪些放弃策略?

  1. AbortPolicy:ThreadPoolExecutor中默认的拒绝策略就是AbortPolicy。直接抛出异常也不处理
  2. CallerRunsPolicy:在任务被拒绝添加后,会调用当前线程池的所在的线程去执行被拒绝的任务;主线程直接执行该任务,执行完之后尝试添加下一个任务到线程池中,这样可以有效降低向线程池内添加任务的速度。

  3. DiscardPolicy:采用这个拒绝策略,会让被线程池拒绝的任务直接抛弃,不会抛异常也不会执行。

  4. DiscardOldestPolicy:当任务被拒绝添加时,会抛弃任务队列中最旧的任务也就是最先加入队列的,再把这个新任务加入。

建议大家用 CallerRunsPolicy 策略,因为当队列中的任务满了之后,如果直接抛异常,那么这个任务就会被丢弃。如果是 CallerRunsPolicy 策略,则会用主线程去执行,也就是同步执行,这样操作最起码任务不会被丢弃。

十、线程池 和 ThreadLocal

首先,线程池中的线程是可以被复用的。

线程池中的线程在任务执行完成后会被复用,所以在线程执行完成时,要对 ThreadLocal 进行清理(清除掉与本线程相关联的 value 对象)。不然,被复用的线程去执行新的任务时会使用被上一个线程操作过的 value 对象,从而产生不符合预期的结果。

之前项目中就出现过这个问题,A登录后调用服务,在线程池中使用了一个线程来执行任务,任务执行完了线程重新回到线程池。这时候B也登录,也使用到了刚刚A使用过的线程,但由于A的任务完成时没有对该线程的ThreadLocal进行清理,这时该线程的ThreadLocal中保存的还是A线程使用时set的value值,这个值主要包含当前登录人的id、姓名、token等信息,在B过来使用时,显示的登录等业务信息还是A的,造成严重bug。

举个例子:

public class ThreadLocalVariableHolder {
    private static ThreadLocal<Integer> variableHolder = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static int getValue() {
        return variableHolder.get();
    }

    public static void remove() {
        variableHolder.remove();
    }

    public static void increment() {
        variableHolder.set(variableHolder.get() + 1);
    }

   public static void main(String[] args) {
    ExecutorService executor = Executors.newCachedThreadPool();
    for (int i = 0; i < 5; i++) {
        executor.execute(() -> {
            long threadId = Thread.currentThread().getId();
            int before = getValue();
            increment();
            int after = getValue();
            System.out.println("threadId: " + threadId + ", before: " + before + ", after: " + after);
        });
    }
    executor.shutdown();
}
threadId: 10, before: 0, after: 1
threadId: 11, before: 0, after: 1
threadId: 12, before: 0, after: 1
threadId: 12, before: 1, after: 2
threadId: 11, before: 1, after: 2

 既然是为每个线程都提供一个副本,那每个线程的before,应该从初始化的0开始,为啥会出现 before 不为 0 的情况呢?

由上面的执行结果可以看出,id 为 11 和 12 的线程被复用。线程池在复用线程执行任务时使用被之前的线程操作过的 value 对象。因此,在每个线程执行完成时,应该清理 ThreadLocal。具体做法如下:

public static void main(String[] args) {
    ExecutorService executor = Executors.newCachedThreadPool();
    for (int i = 0; i < 100; i++) {
        executor.execute(() -> {
            try {
                long threadId = Thread.currentThread().getId();
                int before = getValue();
                increment();
                int after = getValue();
                System.out.println("threadId: " + threadId + ", before: " + before + ", after: " + after);
            } finally {
                // 清理线程本地存储
                remove();
            }
        });
    }
    executor.shutdown();
}

补充一、手动创建线程池好吗?

项目中创建多线程时,使用常见的三种线程池创建方式,单一、可变、定长都有一定问题,原因是FixedThreadPool和SingleThreadExecutor底层都是用LinkedBlockingQueue实现的,这个队列最大长度为Integer.MAX_VALUE,容易导致OOM。所以实际生产一般自己通过ThreadPoolExecutor的7个参数手动创建,自定义线程池:

ExecutorService threadPool=new ThreadPoolExecutor(2,5,
                        1L,TimeUnit.SECONDS,
                        new LinkedBlockingQueue<>(3),
                        Executors.defaultThreadFactory(),
                        new ThreadPoolExecutor.AbortPolicy());

使用线程池时,阿里巴巴P3C代码规约插件将提示:

手动创建线程池--不允许Executors.newFixedThreadPool创建线程池

补充二、怎么合理设置线程池的参数?

为了说明合理设置的条件,我们首先确定有以下几个相关参数:
1.tasks,程序每秒需要处理的最大任务数量(假设系统每秒任务数为100~1000)
2.tasktime,单线程处理一个任务所需要的时间(每个任务耗时0.1秒)
3.responsetime,系统允许任务最大的响应时间(每个任务的响应时间不得超过2秒)

corePoolSize:

每个任务需要tasktime秒处理,则每个线程每秒可处理1/tasktime个任务。系统每秒有tasks个任务需要处理,则需要的线程数为:tasks/(1/tasktime)。即tasks*tasktime个线程数。假设系统每秒任务数为100到1000之间,每个任务耗时0.1秒,则需要100x0.1至1000x0.1,即10到100个线程。那么corePoolSize应该设置为大于10。
具体数字最好根据二八原则,即80%情况下系统每秒任务数,若系统80%的情况下任务数小于200,最多时为1000,则corePoolSize可设置为20。

queueCapacity:任务队列的长度

任务队列的长度要根据核心线程数,以及系统对任务响应时间的要求有关。队列长度可以设置为(corePoolSize/tasktime)responsetime: (20/0.1)2=400,即队列长度可设置为400。
如果队列长度设置过大,会导致任务响应时间过长,如以下写法:
LinkedBlockingQueue queue = new LinkedBlockingQueue();
这实际上是将队列长度设置为Integer.MAX_VALUE,将会导致线程数量永远为corePoolSize,再也不会增加,当任务数量陡增时,任务响应时间也将随之陡增。

maxPoolSize:最大线程数

当系统负载达到最大值时,核心线程数已无法按时处理完所有任务,这时就需要增加线程。每秒200个任务需要20个线程,那么当每秒达到1000个任务时,则需要(1000-queueCapacity)*(20/200),即60个线程,可将maxPoolSize设置为60。

keepAliveTime:

线程数量只增加不减少也不行。当负载降低时,可减少线程数量,如果一个线程空闲时间达到keepAliveTiime,该线程就退出。默认情况下线程池最少会保持corePoolSize个线程。keepAliveTiime设定值可根据任务峰值持续时间来设定。

以上关于线程数量的计算并没有考虑CPU的情况。若结合CPU的情况,比如,当线程数量达到50时,CPU达到100%,则将maxPoolSize设置为60也不合适,此时若系统负载长时间维持在每秒1000个任务,则超出线程池处理能力,应设法降低每个任务的处理时间(tasktime)。

猜你喜欢

转载自blog.csdn.net/weixin_41231928/article/details/107008735
今日推荐