「并发」线程池

60 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第11天,点击查看活动详情

存在的意义:

1、为了简化对线程的操作

2、为了减少开发人员创建线程过多而带来问题

3、为了减少在需要创建多个线程的时候,带来时间的开销

如何使用线程池

使用Executors创建线程操作

实际开发中不建议使用,因为各种方法的创建都有局限性,不能满足多变业务情况的需要。

1、创建线程池对象

//根据传入的参数固定线程数,也就是给定的线程数越多处理线程越多。然后工作队列会按照线程数每次分配固定的任务数。缺点:线程处理不过来任务,任务就放到了队列中。会导致队列占用内存过大导致OOM
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(100);
//没有核心线程,最大线程为2^31-1。且队列为非阻塞队列。也就是任务再多也没有上限,不会出现线程等待的情况。缺点:易出现CPU满(不安全,线程的创建被业务关联,容易造成CPU卡死,比如我100万个任务,可能就创建超过50w的线程,线程是被CPU调度执行的,和内存没关系。)
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
//线程池中只有一个线程,依次处理。缺点:线程处理不过来任务,任务就放到了队列中。会导致队列占用内存过大导致OOM 
        ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();

2、使用线程池提交线程任务

newFixedThreadPool.execute(new MyTask(i));

自定义的线程管理对象

直接调用ThreadPoolExecutor创建线程池管理对象。根据业务的特性设置其corePoolSize、maximumPoolSize等参数,所以这个时候就需要深入理解ThreadPoolExecutor

上面的newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor其底层都是调用的ThreadPoolExecutor构造方法

    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;
    }
  • corePoolSize,核心线程数。可以理解成初始化线程数量
  • maximumPoolSize,最大允许创建的线程数。在核心线程数之外,如果不满足业务需要,就会继续创建线程。直到线程总数达到该值为止。
  • keepAliveTime,线程空闲多久释放
  • unit,keepAliveTime的单位
  • workQueue,工作队列,也就是任务超过了每个线程执行一个的时候进行阻塞等待时的队列
  • threadFactory,线程工厂,创建线程的地方
  • handler,如果任务超过阻塞队列允许承受的最大范围,选择的四种拒绝策略

概述任务的执行(100个任务,corePoolSize=10、maximumPoolSize=50、keepAliveTime=10s、workQueue.size()=10):

1、将任务分配给10个核心线程进行执行(0-10)

2、将剩下90个任务放到workqueue中,发现只能放十个(10-21)

3、没记录一个任务就创建一个线程,直到新创建的线程+核心线程数=maximumPoolSize=50为止。这个时候是(21-70)

4、发现还有没办法分配的任务,直接抛出异常,异常处理方式就俺咋后配置的handler拒绝策略来执行

3.1.1提交优先级和执行优先级

队列为ArrayBlockingQueue时(基本原理,不同的队列可能根据参数导致结果不太一样)

提交优先级:

1、核心线程

2、工作队列

3、非核心线程

执行优先级

1、核心线程

2、非核心线程

3、工作队列

100个糖小明往嘴巴里塞了第1-10颗,往妈妈口袋里塞了第11-20颗。然后发现塞不下了,就喊了妹妹小红过来一起吃,小红就往嘴里也塞了第21-30颗。然后剩下就不要了。

这个时候糖被拿出来的顺序是1-10->11-20->21-30

但是糖被吃掉的顺序是1-10->21-30->11-20

3.1.2 拒绝策略

AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 (默认),针对的是关键业务。如果丢弃会出现问题的情况

DiscardPolicy:丢弃任务,但是不抛出异常。针对可执行可不执行或者有补偿措施的任务,比如(只是举个例子表达程度)日志记录往IO文件里面写入,真实丢了就丢了不能因为这个导致线上系统报警吧。

DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。喜新厌旧

CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务

3.1.3 工作队列workQueue

  • 直接提交队列(SynchronousQueue):没有容量,执行一个插入操作就需要执行一个删除操作,否则就会阻塞等待;1
  • 有界任务队列(ArrayBlockingQueue):突出的特点就是上面的提交优先级。任务多余corePoolSize就会存入队列中,如果超过队列就会创建线程直到达到maximumPoolSize;有限大
  • 无界任务队列(LinkedBlockingQueue):没有maximumPoolSize的概念,队列会保存所有除了核心线程管理的任务。(易出现任务一直增长直到资源耗尽的情况);无限大
  • 优先任务队列(PriorityBlockingQueue):正常队列是先进先出。这个队列可以自定义任务的优先级来执行;自定义执行顺序