Java线程池

1 线程池
  • 出现背景:对于服务器端的程序,经常面对的是客户端传入的短小的任务(执行时间多,工作内容较单一),需要服务器快速处理并返回结果。如果服务器每次接受到一个任务,创建一个线程然后执行,当面对成千上万个任务递交给服务器时,将会创建数以万计的线程。会使操作系统频繁的进行线程上下文切换,线程的创建和销毁也需要耗费系统资源。

  • 线程池能够很好的解决这个问题,它预先创建若干数量的线程,并且不能由用户直接对线程的创建进行控制,在这个前提下重复使用固定或较为固定数目的线程来完成任务。一方面消除了频繁创建和消亡线程的系统开销,另一方面,面对过量任务的提交能够平缓的劣化。

    把创建和销毁分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有创建销毁的开销了。

  • java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池。

  • 客户端通过execute(Job)方法将Job提交到线程池执行。

  • 一个线程池包括以下四个基本组成部分:

    1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
    2、工作者线程(Worker):线程池中线程,一个重复执行Job的线程。在没有任务时处于等待状态,释放cpu资源,工作队列中有任务时才唤醒对应线程从队列中取出消息进行执行。;每个由客户端提交的Job都将进入一个工作队列中等待工作者线程的处理。
    3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
    4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。

  • 线程池的本质就是使用了一个线程安全的工作队列链接工作者线程和客户端线程,客户端线程将任务放入工作队列中返回,工作者线程则不断地从工作队列中取出工作并执行。工作队列为空时工作者线程进入等待状态。

2 Executor接口

在这里插入图片描述

  • 要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在Executors类里面提供了一些静态工厂,生成一些常用的线程池。但不建议使用 Executors静态工厂构建线程池:

    Executors返回的线程池对象的弊端如下:
    1:FixedThreadPool 和 SingleThreadPool:
    允许的请求队列(底层实现是LinkedBlockingQueue)长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
    2:CachedThreadPool 和 ScheduledThreadPool
    允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

  • 可以自己直接调用ThreadPoolExecutor的构造函数来自己创建线程池。在创建的同时,给BlockQueue指定容量就可以了。

    	public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, 
    			long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
    	    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
    	         Executors.defaultThreadFactory(), defaultHandler);
    	}
    
  • corePoolSize(线程池基本大小):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,(除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。)

  • maximumPoolSize(线程池最大大小):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。

  • keepAliveTime(线程存活保持时间)当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。

  • workQueue(任务队列):用于传输和保存等待执行任务的阻塞队列

    为什么使用阻塞队列?

    • 线程池创建线程需要获取mainlock这个全局锁,影响并发效率,阻塞队列可以很好的缓冲。在ThreadPoolExecutor完成预热后(运行线程数>=核心线程数),几乎所有的execute()方法调用,都是执行步骤二,不需要获取全局锁。
    • 线程若是无限制的创建,可能会导致内存占用过多而产生OOM(Out Of Memory),并且会造成cpu过度切换,创建的开销也较高。

    包括:

    • ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO对元素排序。
      当正在执行的线程数等于corePoolSize时,多余的元素缓存在ArrayBlockingQueue队列中等待有空闲的线程时继续执行,当ArrayBlockingQueue已满时,加入ArrayBlockingQueue失败,会开启新的线程去执行,当线程数已经达到最大的maximumPoolSizes时,再有新的元素尝试加入ArrayBlockingQueue时会报错。
      背后的理念是:该池大部分时间仅使用核心线程,即使有适量的任务在队列中等待运行。这时线程池就可以用作节流阀。如果挤压的请求变得非常多,这时该池就会尝试运行更多的线程来清理;这时第二个节流阀maximumPoolSizes就起作用了。
    • LinkedBlockingQueue:基于链表的阻塞队列,按FIFO排序元素。LinkedBlockingQueue是无界的,可以不指定队列的大小,但是默认是Integer.MAX_VALUE。当然也可以指定队列大小,从而成为有界的。
      任务队列采用LinkedBlockingQueue队列的话,那么不会拒绝任何任务(因为队列大小没有限制),这种情况下,ThreadPoolExecutor最多仅会按照最小线程数来创建线程,也就是说线程池大小(maximumPoolSize)被忽略了。
    • SynchronousQueue:一个不存储元素的阻塞队列,会直接将任务交给工作者线程,必须等队列中的添加元素被执行后才能继续添加新的元素。(分为公平和非公平)
  • threadFactory(线程工厂):用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。

  • handler(线程饱和策略):当线程池和队列都满了,再加入线程会执行此策略。

3 线程池工作流程(执行execute()方法)
  1. 当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。(创建新的线程需要获取全局锁)
  2. 当线程池达到corePoolSize时,新提交任务将被放入workQueue中(Blocking Queue),等待线程池中任务调度执行
  3. 当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务(同样需要获取全局锁)
  4. 当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理
  5. 当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程
  6. 当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭
    在这里插入图片描述
4 常见线程池
  • newSingleThreadExecutor
    单个线程的线程池,即线程池中每次只有一个线程工作,单线程串行执行任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

    corePoolSize为1;maximumPoolSize为1;keepAliveTime为0L;unit为:TimeUnit.MILLISECONDS;workQueue为:new LinkedBlockingQueue()
    适用于一个任务一个任务执行的场景。

  • newFixedThreadPool
    固定数量的线程池,每个线程存活时间无限,每提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入阻塞队列(无界的阻塞队列)直到前面的任务完成才继续执行。

    corePoolSize为nThread,maximumPoolSize为nThread;keepAliveTime为0L(不限时);unit为:TimeUnit.MILLISECONDS;WorkQueue为:new LinkedBlockingQueue()
    如果LinkedBlockongQueue的容量为Integer.MAX_VALUE,即无界队列,则参数maximumPoolSize、keepAliveTime和饱和策略都无效了,该线程池将不会拒绝任务。
    适用场景:执行长期的任务,性能好很多。

  • newCacheThreadPool(推荐使用)
    可缓存线程池,当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行。

    corePoolSize为0;maximumPoolSize为Integer.MAX_VALUE;keepAliveTime为60L;unit为TimeUnit.SECONDS;workQueue为SynchronousQueue(同步队列),若池中线程空闲时间超过指定大小,则该线程会被销毁。
    适用场景:执行很多短期异步的小程序或者负载较轻的服务器。可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换。

  • newScheduleThreadPool
    大小无限制的线程池,支持定时和周期性的执行线程

    corePoolSize为传递来的参数,maximumPoolSize为Integer.MAX_VALUE;keepAliveTime为0;unit为:TimeUnit.NANOSECONDS;workQueue为:new DelayedWorkQueue() 一个按超时时间升序排序的队列

execute()和submit()方法
  1. execute(),执行一个任务,没有返回值。
  2. submit(),提交一个线程任务,有返回值。

    submit(Callable task)能获取到它的返回值,通过future.get()获取(阻塞直到任务执行完)。一般使用FutureTask+Callable配合使用
    submit(Runnable task, T result)能通过传入的载体result间接获得线程的返回值。
    submit(Runnable task)则是没有返回值的,就算获取它的返回值也是null。

handler饱和策略
  • Abort 策略:默认策略,直接抛出异常。
  • CallerRuns 策略:为调节机制,既不抛弃任务也不抛出异常,而是将某些任务回退到调用者。不会在线程池的线程中执行新的任务,而是在调用execute的线程中运行新的任务。
  • Discard策略:新提交的任务被抛弃。
  • DiscardOldest策略:丢弃的是“队头”的任务,然后尝试提交新的任务。(不适合工作队列为优先队列场景)

【参考文档】
Java线程池详解
《Java并发编程的艺术》
Java线程池面试题
线程池工作队列饱和策略
线程池的三种队列区别

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值