线程池原理

128 阅读3分钟

线程资源比较多,比较重量级,这类的资源,我们经常使用池化技术。我们也要维护线程池的最小线程数和最大线程数。太多了可能会导致线程的上下文切换比较重。严重影响并行效率。还有一点是,请求激增,处理不完的任务,是直接拒绝掉?还是放在缓冲区?这些通用的逻辑 jdk 已经帮我们实现好了。他就是线程池。

线程池 涉及的主要接口和类

  • Excutor : 执行者,顶层接口
  • ExcutorService :接口 API。线程池通用接口
  • Thread Factory : 线程工厂 : 线程具体是怎么创建的,如何给线程设置一些通用参数的。
  • ThreadPollExcutor (最常用)
  • Excutors:工具类,常见线程池。

image.png

Excutor
重要方法说明
void execute(Runnable)运行可执行任务
ExecutorService
重要方法说明
void shutdown()停止接收新任务,原来的任务继续执行。 业务重启或者重新的发布部署,老机器下线,新机器业务上线,我们需要做到优雅停机,执行了一半的业务不被强行打断而导致数据库和我们的业务的一些状态和数据出现不一致的极端情况。
List<Runnable> shutdownNow()停止接受新任务,原来的任务停止执行
boolean isShutdown()
boolean isTerminated()阻塞当前线程,返回是否线程都执行完。
Future<?> submit(Runnable)运行可执行任务
Future<?> submit(Callable)运行可执行任务
Future<?> submit(Runnable,T reuslt)运行可执行任务

future 正常情况下封装的就是我们的返回结果。通过 future.get() 就可以拿到返回结果。我们调用的异步执行的方法很多时候可能会给我们抛出一个异常。future 会把异常也封装起来,特别牛批。execute完全异步的,会直接在任务线程的堆栈里把异常跑出来。在当前的线程里是无法 try catch 住的。 有一点注意的是,future拿到的是一个object对象,在算术异常抛出的时候,会返回一个非数 异常(Nan|Infinity)

重启机器时间不可控的情况,优雅停机 可以这么写 : 先shutdown(),调用awaitTerminaltion阻塞当前的主线程。到达指定时间后不阻塞当前线程。同时返回一个Boolean值,如果所有的线程都停止了返回false,当前的线程池并不是所有的线程池都停止了,已经执行完了。这时候可以强制的再调用一次shutdownNow()

线程池的实现类 ThreadPoolExecutor

提交任务逻辑
  • 判断 corePoolSize
  • 加入 workQueue = 判断 maximumPoolSize
  • 执行拒绝策略处理器

jdk 源码 如下

public void execute(Runnable command) {
    if (command == null) 
        throw new NullPointerException();
    int c = ctl.get();
    // 判断当前正在运行的线程池数量是否小于核心线程数
    if (workerCountOf(c) < corePoolSize) {
      // 增加新线程运行这个任务
      if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
// 最大线程数是否达到了上限?
    else if (!addWorker(command, false))
// 执行拒绝 策略
        reject(command);
}

前面任务处理不过来的时候,可能是由于,IO等待,导致线程暂停,导致任务处理不过来。线程在进行IO等待的时候,他们吧。新增线程就可以利用到前面没有利用到的那一片CPU 资源的时间片。如果前面没有IO等待,新添加线程也没有用,大家也轮不上时间片。整体产生上下文切换可能导致整体的效率更低。

线程池参数
缓冲队列 JDK 中实现为 BlockingQueue BQ。 block 队列写满会被阻塞。

BQ 是 双缓冲队列。 允许两个线程 同时向队列一个存储,一个取出的操作。在保证并发安全的同时,提高了队列的存取效率。

  • ArrayBlockingQueue :如名字一般,内部是数组,固定长度。构造必须指定其大小。所含的对象是FIFO顺序排序的。
  • LinkedBlockingQueue : 大小不固定的BlockingQueue,若构造指定大小,有大小限制。若不指定大小,最大限制为Integer.MAX_VALUE.所含对象是FIFO。
  • PriorityBlockingQueue : 优先级。排序不是FIFO,类似优先级队列。
  • SynchronizedQueue : 特殊,操作必须是放和取交替完成。
拒绝策略 线程池线程都在忙,队列也满了,这时候就要执行拒绝策略。
  • AbortPolicy : 丢弃任务并抛出 拒绝执行异常。默认。虽然丢弃了,但是会发出信号,我们可以用其他方式来处理这个任务。任务不会真正的丢
  • DiscardPolicy : 丢弃任务,不跑出异常。
  • DiscardOldestPolicy : 丢弃队列最前面的任务,重新提交被拒绝的任务。
  • CallerRunsPolicy : 由提交任务的线程处理该任务使用最多
ThreadFactory

批量的用工厂创建一堆具有相同配置,特定属性的这样一组线程。主要设置名字前缀相同,序号不同的线程。

创建线程池

public static ThreadPoolExecutor init() {
    // cpu 核心数
    int coreSize = Runtime.getRuntime().availableProcessors();
    int maxSize = Runtime.getRuntime().availableProcessors() * 2;
    BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(500);
    ThreadFactory threadFactory = new ThreadFactory() {
        private final AtomicInteger cnt = new AtomicInteger();

        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r);
            thread.setName("threadPrefix_" + cnt.incrementAndGet());
            return thread;
        }
    };
    return new ThreadPoolExecutor(coreSize,maxSize,1,TimeUnit.MINUTES,queue,threadFactory);
}

JDK 提供的4种典型线程池

  • newSingleThreadExecutor

单线程的线程池,只有一个线程在工作,相当于单线程处理所有任务。唯一的线程因为异常结束,会有一个新的线程来代替他。保证所有人物的 执行顺序按提交顺序进行。

  • newFixedThreadPool 固定大小的线程池。每次提交一个任务就新创建一个线程池,直到这个线程池大小达到最大大小。保持不变。某个线程异常结束,会补充一个新的线程。core=max
  • newCachedThreadPool 可缓存的线程池。若超过了处理任务的所需要的线程,会回收不会空闲线程(这个时间是60s),空闲线程有缓冲的空间。线程池大小完全依赖于堆空间设置的大小。
  • ScheduledThreadPool 大小无限的线程池,支持定时任务以及周期执行人物的需求。

线程池大小设置经验

  • CPU 核心数。
  1. cpu 密集型 : N N + 1
  2. IO 密集型 : 2N , 2N + 2