Java ThreadPoolExecutor 线程池

文章目录

  • 概述
    • 为什么要用线程池
  • 核心方法讲解
    • ThreadPoolExecutor的数据结构
      • workers
      • workQueue
      • mainLock
      • corePoolSize和maximumPoolSize
      • poolSize
      • allowCoreThreadTimeOut和keepAliveTime
      • threadFactory
      • handler
    • ThreadPoolExecutor最核心的构造方法
    • 构造方法参数讲解
  • Executors提供的线程池配置方案
    • 固定线程数目的线程池
      • LinkedBlockingQueue
      • LinkedBlockingDeque
    • 无缓冲功能的线程池
      • SynchronousQueue
    • 只支持一个线程的线程池
    • 有定时功能的线程池
  • 排队策略
  • 如何设置参数
    • 默认值
    • 如何来设置
    • 做几个计算
  • 实例
  • 总结
    • Java线程池缺陷
      • 非核心线程创建时机
      • 排队任务调度策略

概述

ThreadPoolExecutor是线程池类。对于线程池,可以通俗的将它理解为"存放一定数量线程的一个线程集合。线程池允许若个线程同时允许,允许同时运行的线程数量就是线程池的容量;当添加的到线程池中的线程超过它的容量时,会有一部分线程阻塞等待。线程池会通过相应的调度策略和拒绝策略,对添加到线程池中的线程进行管理。"

为什么要用线程池

在Java中,如果每当一个请求到达就创建一个新线程,开销是相当大的。在实际使用中,每个请求创建新线程的服务器在创建和销毁线程上花费的时间和消耗的系统资源,甚至可能要比花在处理实际的用户请求的时间和资源要多得多。

除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个JVM里创建太多的线程,可能会导致系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,服务器应用程序需要一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务,这就是“池化资源”技术产生的原因。
线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使应用程序响应更快。另外,通过适当地调整线程池中的线程数目可以防止出现资源不足的情况。

核心方法讲解

ThreadPoolExecutor的数据结构

Java ThreadPoolExecutor 线程池_第1张图片

// 阻塞队列。
private final BlockingQueue workQueue;
// 互斥锁
private final ReentrantLock mainLock = new ReentrantLock();
// 线程集合。一个Worker对应一个线程。
private final HashSet workers = new HashSet();
// “终止条件”,与“mainLock”绑定。
private final Condition termination = mainLock.newCondition();
// 线程池中线程数量曾经达到过的最大值。
private int largestPoolSize;
// 已完成任务数量
private long completedTaskCount;
// ThreadFactory对象,用于创建线程。
private volatile ThreadFactory threadFactory;
// 拒绝策略的处理句柄。
private volatile RejectedExecutionHandler handler;
// 保持线程存活时间。
private volatile long keepAliveTime;

private volatile boolean allowCoreThreadTimeOut;
// 核心池大小
private volatile int corePoolSize;
// 最大池大小
private volatile int maximumPoolSize;

workers

workers是HashSet类型,即它是一个Worker集合。而一个Worker对应一个线程,也就是说线程池通过workers包含了"一个线程集合"。当Worker对应的线程池启动时,它会执行线程池中的任务;当执行完一个任务后,它会从线程池的阻塞队列中取出一个阻塞的任务来继续运行。
wokers的作用是,线程池通过它实现了"允许多个线程同时运行"

workQueue

workQueue是BlockingQueue类型,即它是一个阻塞队列。当线程池中的线程数超过它的容量的时候,线程会进入阻塞队列进行阻塞等待。
通过workQueue,线程池实现了阻塞功能。

mainLock

mainLock是互斥锁,通过mainLock实现了对线程池的互斥访问。

corePoolSize和maximumPoolSize

corePoolSize是"核心池大小",maximumPoolSize是"最大池大小"。它们的作用是调整"线程池中实际运行的线程的数量"。
例如,当新任务提交给线程池时(通过execute方法)。
如果此时,线程池中运行的线程数量< corePoolSize,则创建新线程来处理请求。
如果此时,线程池中运行的线程数量> corePoolSize,但是却< maximumPoolSize;则仅当阻塞队列满时才创建新线程。
如果设置的 corePoolSize 和 maximumPoolSize 相同,则创建了固定大小的线程池。如果将 maximumPoolSize 设置为基本的无界值(如 Integer.MAX_VALUE),则允许池适应任意数量的并发任务。在大多数情况下,核心池大小和最大池大小的值是在创建线程池设置的;但是,也可以使用 setCorePoolSize(int) 和 setMaximumPoolSize(int) 进行动态更改。

poolSize

poolSize是当前线程池的实际大小,即线程池中任务的数量。

allowCoreThreadTimeOut和keepAliveTime

allowCoreThreadTimeOut表示是否允许"线程在空闲状态时,仍然能够存活";而keepAliveTime是当线程池处于空闲状态的时候,超过keepAliveTime时间之后,空闲的线程会被终止。

threadFactory

threadFactory是ThreadFactory对象。它是一个线程工厂类,“线程池通过ThreadFactory创建线程”。

handler

handler是RejectedExecutionHandler类型。它是"线程池拒绝策略"的句柄,也就是说"当某任务添加到线程池中,而线程池拒绝该任务时,线程池会通过handler进行相应的处理"。
Java ThreadPoolExecutor 线程池_第2张图片
Java ThreadPoolExecutor 线程池_第3张图片
说明:
在"图-01"中,线程池中有N个任务。“任务1”, “任务2”, "任务3"这3个任务在执行,而"任务3"到"任务N"在阻塞队列中等待。正在执行的任务,在workers集合中,workers集合包含3个Worker,每一个Worker对应一个Thread线程,Thread线程每次处理一个任务。
当workers集合中处理完某一个任务之后,会从阻塞队列中取出一个任务来继续执行,如图-02所示。图-02表示"任务1"处理完毕之后,线程池将"任务4"从阻塞队列中取出,放到workers中进行处理

ThreadPoolExecutor最核心的构造方法

public ThreadPoolExecutor(int corePoolSize,  
                              int maximumPoolSize,  
                              long keepAliveTime,  
                              TimeUnit unit,  
                              BlockingQueue 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.corePoolSize = corePoolSize;  
        this.maximumPoolSize = maximumPoolSize;  
        this.workQueue = workQueue;  
        this.keepAliveTime = unit.toNanos(keepAliveTime);  
        this.threadFactory = threadFactory;  
        this.handler = handler;  
    }  

构造方法参数讲解

参数名字 作用
corePoolSize 核心线程池大小
maximumPoolSize 最大线程池大小
keepAliveTime 线程池中超过corePoolSize数目的空闲线程最大存活时间;可以allowCoreThreadTimeOut(true)使得核心线程有效时间
TimeUnit keepAliveTime时间单位
workQueue 阻塞任务队列
threadFactory 新建线程工厂
RejectedExecutionHandler 当提交任务数超过maxmumPoolSize+workQueue之和时,任务会交给RejectedExecutionHandler来处理

其中比较容易让人误解的是:corePoolSize,maximumPoolSize,workQueue之间关系。
一个任务(即调用的一个方法)通过 execute(Runnable)方法被添加到线程池,任务就是一个 Runnable类型的对象,任务的执行方法就是Runnable类型对象的run()方法。

当一个任务通过execute(Runnable)方法欲添加到线程池时:

1.当线程池中运行的线程数量即使此时线程池中存在空闲线程 (有空闲也新建)
2.当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行
3.当workQueue已满,且maximumPoolSize>corePoolSize时(此时workQueue已满),新提交任务会创建新线程执行任务
4.当线程池中运行的线程数量>maximumPoolSize时,新提交任务由RejectedExecutionHandler处理

5.当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程
6.当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭

线程管理机制图示:
Java ThreadPoolExecutor 线程池_第4张图片

Executors提供的线程池配置方案

固定线程数目的线程池

配置的corePoolSize与maximumPoolSize大小相同,同时使用了一个无界LinkedBlockingQueue存放阻塞任务,因此多余的任务将存在再阻塞队列,不会由RejectedExecutionHandler处理。

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

LinkedBlockingQueue

LinkedBlockingQueue是无界的,是一个无界缓存的等待队列。
基于链表的阻塞队列,内部维持着一个数据缓冲队列(该队列由链表构成)。当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。
LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

LinkedBlockingDeque

是一个由链表结构组成的双向阻塞队列,即可以从队列的两端插入和移除元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。

无缓冲功能的线程池

配置 corePoolSize=0,maximumPoolSize=Integer.MAX_VALUE,keepAliveTime=60s,以及一个无容量的阻塞队列 SynchronousQueue,因此任务提交之后,将会创建新的线程执行;线程空闲超过60s将会销毁

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

当使用SynchronousQueue队列的时候,maximumPoolSize应该设置的略大一些,最大值取决于同时提交任务的最大数。
使用SynchronousQueue的时候,因为当activeCount达到maximumPoolSize的时候,再有任务进来就会发生Reject。因此maximumPoolSize可以设置大一些,那么设置多大呢?我们可以通过检测activeCount/maximumPoolSize的值来决定是否需要将maximumPoolSize的值调整的大一些,而这一时刻可以通过报警告知我们。


public class ThreadPoolCached {
    public static void main(String[] args) {
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        //当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程
//        ExecutorService cachedThreadPool = Executors.newFixedThreadPool(5);
//        ExecutorService cachedThreadPool = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            try {
//                System.out.println(index + "当前线程1" + Thread.currentThread().getName());

                //newCachedThreadPool  不sleep 会使用多个线程,任务长会将使用完的线程重新利用,而不是新建线程
                Thread.sleep(index * 100);
            } catch (Exception e) {
                e.printStackTrace();
            }

            cachedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(index + "当前线程" + Thread.currentThread().getName());
                }
            });
        }
        cachedThreadPool.shutdown();

    }
}

Java ThreadPoolExecutor 线程池_第5张图片
发现10个线程都是使用的线程1,线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程,让之前线程空闲

SynchronousQueue

SynchronousQueue 内部没有容量,但是由于一个插入操作总是对应一个移除操作,反过来同样需要满足。那么一个元素就不会再SynchronousQueue 里面长时间停留,一旦有了插入线程和移除线程,元素很快就从插入线程移交给移除线程。也就是说这更像是一种信道(管道),资源从一个方向快速传递到另一方向。显然这是一种快速传递元素的方式,也就是说在这种情况下元素总是以最快的方式从插入着(生产者)传递给移除着(消费者),这在多任务队列中是最快处理任务的方式。在线程池里的一个典型应用是newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收。
可以这样来理解:生产者和消费者互相等待对方,握手,然后一起离开。

只支持一个线程的线程池

配置corePoolSize=maximumPoolSize=1,无界阻塞队列LinkedBlockingQueue;保证任务由一个线程串行执行

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

有定时功能的线程池

配置corePoolSize,无界延迟阻塞队列DelayedWorkQueue;有意思的是:maximumPoolSize=Integer.MAX_VALUE,由于DelayedWorkQueue是无界队列,所以这个值是没有意义的。

public class ScheduledThreadPoolTest {
    public static void main(String[] args) throws InterruptedException {
        // 创建大小为5的线程池
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);

        for (int i = 0; i < 3; i++) {
            Task worker = new Task("task-" + i);
            // 只执行一次
//          scheduledThreadPool.schedule(worker, 5, TimeUnit.SECONDS);
            // 周期性执行,每5秒执行一次,直到主线程里线程池shutdown()
            scheduledThreadPool.scheduleAtFixedRate(worker, 0,8, TimeUnit.SECONDS);
        }

        Thread.sleep(10000);

        System.out.println("Shutting down executor...");
        // 关闭线程池
        scheduledThreadPool.shutdown();
        boolean isDone;
        // 等待线程池终止
        do {
            isDone = scheduledThreadPool.awaitTermination(1, TimeUnit.DAYS);
            System.out.println("awaitTermination...");
        } while(!isDone);

        System.out.println("Finished all threads");
    }
}

排队策略

排队有三种通用策略:
直接提交。工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize 的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列
有界队列。当使用有限的 maximumPoolSizes 时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。 如果任务频繁阻塞(例如,如果它们是 I/O 边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。

如何设置参数

默认值

corePoolSize=1
queueCapacity=Integer.MAX_VALUE
maxPoolSize=Integer.MAX_VALUE
keepAliveTime=60s
allowCoreThreadTimeout=false
rejectedExecutionHandler=AbortPolicy()

如何来设置

需要根据几个值来决定
tasks :每秒的任务数,假设为500~1000
taskcost:每个任务花费时间,假设为0.1s
responsetime:系统允许容忍的最大响应时间,假设为1s

做几个计算

corePoolSize = 每秒需要多少个线程处理?
threadcount = tasks/(1/taskcost) =tasks*taskcost = (500~1000)*0.1 = 50~100 个线程。corePoolSize设置应该大于50
根据8020原则,如果80%的每秒任务数小于800,那么corePoolSize设置为80即可
queueCapacity = (coreSizePool/taskcost)responsetime
计算可得 queueCapacity = 80/0.1
1 = 80。意思是队列里的线程可以等待1s,超过了的需要新开线程来执行
切记不能设置为Integer.MAX_VALUE,这样队列会很大,线程数只会保持在corePoolSize大小,当任务陡增时,不能新开线程来执行,响应时间会随之陡增。
maxPoolSize = (max(tasks)- queueCapacity)/(1/taskcost)
计算可得 maxPoolSize = (1000-80)/10 = 92
(最大任务数-队列容量)/每个线程每秒处理能力 = 最大线程数
rejectedExecutionHandler:根据具体情况来决定,任务不重要可丢弃,任务重要则要利用一些缓冲机制来处理
keepAliveTime和allowCoreThreadTimeout采用默认通常能满足
以上都是理想值,实际情况下要根据机器性能来决定。如果在未达到最大线程数的情况机器cpu load已经满了,则需要通过升级硬件(呵呵)和优化代码,降低taskcost来处理。

实例

private final ExecutorService es = new ThreadPoolExecutor(10, 30, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(50));

@Override
public Response> getFoodOwnersIdByPoiShopIds(List poiShopIds, final Integer platform){
        List shopOwnerDTOs = Lists.newArrayList();
        try {
            for (Integer poiShopId : poiShopIds) {
                final int shopId = poiShopId;
                Future shopOwnerCallable = es.submit(new Callable() {
@Override
public Object call() throws Exception {
     return getFoodOwnerIdByPoiShopId(shopId, platform);
       }
   });

  Response response = (Response) shopOwnerCallable.get();
   if(response.isSuccess()) {
   shopOwnerDTOs.add(response.getObj());
                }
            }
        } catch (ExecutionException ex) {
            logger.error(ex.getMessage(),ex);
        } catch (InterruptedException e) {
            logger.error(e.getMessage(),e);
        }
        return Response.success(shopOwnerDTOs);
    }
 
  

总结

1.用ThreadPoolExecutor自定义线程池,看线程的用途,如果任务量不大,可以用无界队列,如果任务量非常大,要用有界队列,防止OOM(out of memory)
2.当使用有界队列的时候,corePoolSize设置的应该尽可能和maximumPoolSize相等。使用有界队列的时候,因为只有当队列满的时候才会创建大于corePoolSize大小的线程数,而队列有任务堆积已经意味着当前线程池中的工作线程处于过载状态,我们需要关注这个事件,而maximumPoolSize就基本没有用了,因此我们建议当使用有界队列的时候,corePoolSize的大小可以和maximumPoolSize的大小相等。
3.如果任务量很大,还要求每个任务都处理成功,要对提交的任务进行阻塞提交,重写拒绝机制,改为阻塞提交。保证不抛弃一个任务
4.最大线程数一般设为2N+1最好,N是CPU核数 (Intel 赛扬G460是单核心,双线程的CPU,Intel 酷睿i3 3220是双核心 四线程,Intel 酷睿i7 4770K是四核心 八线程 ,Intel 酷睿i5 4570是四核心 四线程等等)
5.核心线程数,看应用,如果是任务,一天跑一次,设置为0,合适,因为跑完就停掉了,如果是常用线程池,看任务量,是保留一个核心还是几个核心线程数
6.如果要获取任务执行结果,用CompletionService,但是注意,获取任务的结果的要重新开一个线程获取,如果在主线程获取,就要等任务都提交后才获取,就会阻塞大量任务结果,队列过大OOM(out of memory),所以最好异步开个线程获取结果。

Java线程池缺陷

非核心线程创建时机

非核心线程在队列满时触发创建,在瞬时冲高情况下,队列被占满,但新创建的线程来不及消费等待队列中的任务,新任务被拒绝
举个例子,假设有一个线程池corePoolSize=1,maximumPoolSize=2,队列长度为10。在绝大多数情况下,添加任务的速度为每5秒1条,单任务处理时间为1秒,此时线程池中只有一个核心线程。但某个时间段内,任务生产速度增加,每秒钟任务添加速度增长为每500ms添加1条,单任务处理时间不变,持续时间为10分钟。基于当前的策略,行为如下:

等待队列中任务开始累积,10秒后任务队列满。
第11秒,创建一个新线程处理新任务,第11.5秒新任务到来,此时2个线程均处于busy状态,此时任务丢弃。之后队列始终处于满状态,由于调度时差任务可能进一步丢失。10分钟后开始队列逐步被清空。
这里举了最简单的场景,此场景下始终处于满队列状态,现实中瞬时冲高要比示例中的更复杂,包括任务添加频率不固定,每个任务处理时间的随机性等。

排队任务调度策略

任务的执行顺序和任务的添加顺序是不一致的,这可能导致务堵塞。
为阐述方便,本节先定义一个最简配置线程池配置如下:
核心线程数:1。
队列大小:10。
最大线程数:2。

非核心线程在队列满时触发创建,并执行当前的任务,但队列中的任务依旧处于排队状态。举例说明:
当前核心线程已满,队列(队列大小为10)中处于排队的任务编号分别为任务20-29。
当任务30到来时,队列插入失败,创建新线程,此时新线程处理任务30,而非任务20。

换句话说,任务的执行顺序和任务的添加顺序是不一致的,这可能导致务堵塞。想象队列中依次添加两个任务A、B,并且B执行过程中需要等待任务A的执行结果。此时,如果任务B先于任务A执行,任务B被堵塞,线程池调度效率降低。

参考:
https://www.cnblogs.com/skywang12345/p/3509941.html (Java多线程系列–“JUC线程池”02之 线程池原理(一))
https://www.cnblogs.com/duanxz/p/3252267.html (阻塞队列之三:SynchronousQueue同步队列 阻塞算法的3种实现)
https://blog.csdn.net/q438944209/article/details/82923024 (线程池的使用(newCachedThreadPool))
https://www.cnblogs.com/feiyun126/p/7686302.html (ThreadPoolExecutor的三种队列SynchronousQueue,LinkedBlockingQueue,ArrayBlockingQueue)
https://www.jianshu.com/p/896b8e18501b (Java线程池分析及策略优化)
http://825635381.iteye.com/blog/2184680
http://blog.csdn.net/wangwenhui11/article/details/6760474
http://blog.csdn.net/mccand1234/article/details/51972820
http://blog.csdn.net/yu132563/article/details/45222935
https://www.cnblogs.com/waytobestcoder/p/5323130.html (ThreadPoolExecutor线程池参数设置技巧)

你可能感兴趣的:(java,面试)