NiceCui - 崔世峰

一个想给航空母舰造轮子拧螺丝的程序员

疯狂造轮子 - 公众号

线程池详解

线程池概述

  1. 线程池就是一个管理线程的池子,可以降低创建和销毁线程带来的资源消耗

因为线程其实也是一个对象,创建一个对象,需要经过类加载过程,销毁一个对象,需要走GC垃圾回收流程,都是需要资源开销的。

  1. 提高响应速度,任务到达了相对于从线程池取线程,自己创建线程肯定慢很多
  2. 重复利用,线程用完了再放回池子,达到了重复利用的效果

线程池执行

打个比喻

  • 核心线程比作公司正式员工
  • 非核心线程比作外包员工
  • 阻塞队列比作需求池
  • 提交任务比作提需求

正式执行

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,long keepAliveTime,TimeUnit unit,
   BlockingQueue<Runnable> workQueue,
   ThreadFactory threadFactory,
   RejectedExecutionHandler handler) 
corePoolSize     核心线程数
maximumPoolSize  线程池最大线程数
keepAliveTime    空闲线程存活时间
TimeUnit         线程空闲存活时间单位
workQueue        存放任务的阻塞队列
threadFactory    线程工厂
handler          饱和策略
  • 提交一个任务,线程池里存活的核心线程数小于线程数corePoolSize时,线程池会创建一个核心线程去处理提交的任务。
  • 如果线程池核心线程数已满,即线程数已经等于corePoolSize,一个新提交的任务,会被放进任务队列workQueue排队等待执行。
  • 当线程池里面存活的线程数已经等于corePoolSize了,并且任务队列workQueue也满,判断线程数是否达到maximumPoolSize,即最大线程数是否已满,如果没到达,创建一个非核心线程执行提交的任务。
  • 如果当前的线程数达到了maximumPoolSize,还有新的任务过来的话,直接采用拒绝策略处理。

几种饱和策略

AbortPolicy         抛出一个异常,默认的
DiscardPolicy       直接丢弃任务
DiscardOldestPolicy 丢弃队列里最老的任务,将当前这个任务继续提交给线程池
CallerRunsPolicy    交给线程池调用所在的线程进行处理

线程池异常处理

由于在线程池调用线程处理任务过程中出现的异常可能会被线程池捕获,所以对于任务的执行可能是无感知的,因此我们需要考虑线程池异常情况。

方法一:

@Test
public void test1() throws Exception {
    ExecutorService executorService = Executors.newFixedThreadPool(5);
    for (int i = 0; i < 5; i++) {
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("name: " + Thread.currentThread().getName());
                    Object a = null;
                    System.out.println(a.hashCode());
                } catch (Exception e) {
                    System.out.println(e);
                }
            }
        });
    }
}

方法二:

@Test
public void test2() throws Exception {
    ExecutorService executorService = Executors.newFixedThreadPool(5);
    for (int i = 0; i < 20; i++) {
        Future<?> future = executorService.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("name: " + Thread.currentThread().getName());
                Object a = null;
                System.out.println(a.hashCode());
            }
        });
        try {
            future.get();
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}

线程池的工作队列

  • ArrayBlockingQueue
  • LinkedBlockingQueue
  • SynchronousQueue
  • DelayQueue
  • PriorityBlockingQueue

ArrayBlockingQueue

  • 初始化一定容量的数组
  • 使用一个重入锁,默认使用非公平锁,入队和出队共用一个锁,互斥
  • 是有界设计,如果容量满无法继续添加元素直至有元素被移除
  • 使用时开辟一段连续的内存,如果初始化容量过大容易造成资源浪费,过小易添加失败

LinkedBlockingQueue

  • 使用链表数据结构
  • 非连续性内存空间
  • 使用两个重入锁分别控制元素的入队和出队,用Condition进行线程间的唤醒和等待
  • 有边界的,在默认构造方法中容量是Integer.MAX_VALUE

SynchronousQueue

  • 内部容量是0
  • 每次删除操作都要等待插入操作
  • 每次插入操作都要等待删除操作
  • 一个元素,一旦有了插入线程和移除线程,那么很快由插入线程移交给移除线程,这个容器相当于通道,本身不存储元素
  • 在多任务队列,是最快的处理任务方式。

PriorityBlockingQueue

  • 无边界设计,但容量实际是依靠系统资源影响
  • 添加元素,如果超过1,则进入优先级排序

DelayQueue

  • 无边界设计
  • 添加(put)不阻塞,移除阻塞
  • 元素都有一个过期时间
  • 取元素只有过期的才会被取出

常用的线程池

  • newFixedThreadPool (固定数目线程的线程池)
  • newCachedThreadPool (可缓存线程的线程池)
  • newSingleThreadExecutor (单线程的线程池)
  • newScheduledThreadPool (定时及周期执行的线程池)

newFixedThreadPool

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

特点

  1. 核心线程数和最大线程数大小一样
  2. 没有所谓的非空闲时间,即keepAliveTime为0
  3. 阻塞队列为无界队列LinkedBlockingQueue

工作机制:

  • 提交任务
  • 如果线程数少于核心线程,创建核心线程执行任务
  • 如果线程数等于核心线程,把任务添加到LinkedBlockingQueue阻塞队列
  • 如果线程执行完任务,去阻塞队列取任务,继续执行。

newCachedThreadPool

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

线程池特点

  • 核心线程数为0
  • 最大线程数为Integer.MAX_VALUE
  • 阻塞队列是SynchronousQueue
  • 非核心线程空闲存活时间为60秒

工作机制:

  • 提交任务
  • 因为没有核心线程,所以任务直接加到SynchronousQueue队列。
  • 判断是否有空闲线程,如果有,就去取出任务执行。
  • 如果没有空闲线程,就新建一个线程执行。
  • 执行完任务的线程,还可以存活60秒,如果在这期间,接到任务,可以继续活下去;否则,被销毁。

使用场景

用于并发执行大量短期的小任务。

使用SynchronousQueue作为工作队列,工作队列本身并不限制待执行的任务的数量。但此时需要限定线程池的最大大小为一个合理的有限值,而不是Integer.MAX_VALUE,否则可能导致线程池中的工作者线程的数量一直增加到系统资源所无法承受为止。

如果应用程序确实需要比较大的工作队列容量,而又想避免无界工作队列可能导致的问题,不妨考虑SynchronousQueue。SynchronousQueue实现上并不使用缓存空间

newSingleThreadExecutor

线程池特点

  • 核心线程数为1
  • 最大线程数也为1
  • 阻塞队列是LinkedBlockingQueue
  • keepAliveTime为0
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>(),
                                threadFactory));
}

工作机制

  • 提交任务
  • 线程池是否有一条线程在,如果没有,新建线程执行任务
  • 如果有,讲任务加到阻塞队列
  • 当前的唯一线程,从队列取任务,执行完一个,再继续取,一个人(一条线程)夜以继日地干活。

使用场景

适用于串行执行任务的场景,一个任务一个任务的执行

newScheduledThreadPool

线程池特点

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue());
}
  • 最大线程数为Integer.MAX_VALUE
  • 阻塞队列是DelayedWorkQueue
  • keepAliveTime为0
  • scheduleAtFixedRate() :按某种速率周期执行
  • scheduleWithFixedDelay():在某个延迟后执行

工作机制

  • 添加一个任务
  • 线程池中的线程从 DelayQueue 中取任务
  • 线程从 DelayQueue 中获取 time 大于等于当前时间的task
  • 执行完后修改这个 task 的 time 为下次被执行的时间
  • 这个 task 放回DelayQueue队列中

scheduleWithFixedDelay

  • 无论任务执行时间长短,都是当第一个任务执行完成之后,延迟指定时间再开始执行第二个任务

scheduleAtFixedRate

  • 在任务执行时间小于间隔时间的情况下,程序以起始时间为准则,每隔指定时间执行一次,不受任务执行时间影响
  • 当执行任务时间大于间隔时间,此方法不会重新开启一个新的任务进行执行,而是等待原有任务执行完成,马上开启下一个任务进行执行。此时,执行间隔时间已经被打乱
posted @ 2019-11-25 18:08  NiceCui  阅读(414)  评论(0编辑  收藏  举报

个人博客主页