Java 线程池原理与使用

作者: Tim在路上 | 来源:发表于2019-07-28 21:28 被阅读6次

    在java 中我们会一般要求创建线程必须使用线程池,因为这样可以避免资源消耗,通过重复利用已经创建的线程来降低线程创建和销毁所造成的消耗, 其次当任务到达时任务可以不用等到线程创建就立即执行,最后可以提高线程的管理性,管控线程,不让线程无限制创建.

    线程池的创建

    我们可以使用 ThreadPoolExecutor 创建线程池

    这里我们不适用Executors 来创建线程池,是因为,Executors 使用默认参数不用自定义参数,

    自己经常使用Executors提供的工厂方法创建线程池,所以忽略了线程池内部的实现,同时可以规避规避资源耗尽的风险.

    new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime,
    milliseconds,runnableTaskQueue, handler);
    
    1. corePoolSize(线程池的基本大小): 当提交一个任务到线程池时,线程
      池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务
      也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。
      如果调用了线程池的 prestartAllCoreThreads 方法,线程池会提前创建
      并启动所有基本线程。

    2. runnableTaskQueue (任务队列):用于保存等待执行的任务的阻塞队列。
      可以选择以下几个阻塞队列。

    ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队
    列按 FIFO(先进先出)原则对元素进行排序。

    LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按
    FIFO (先进先出) 排序元素,吞吐量通常要高于
    ArrayBlockingQueue。静态工厂方法
    Executors.newFixedThreadPool()使用了这个队列。

    SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必
    须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,
    吞吐量通常要高于 LinkedBlockingQueue,静态工厂方法
    Executors.newCachedThreadPool 使用了这个队列。

    PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

    1. maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。
      如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建
      新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就
      没什么效果。

    2. ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创
      建出来的线程设置更有意义的名字。

    3. RejectedExecutionHandler(饱和策略): 当队列和线程池都满了,默认 AbortPolicy:直接抛出异常,
      还可设置 CallerRunsPolicy:只用调用者所在线程来运行任务。
      o DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前
      任务。
      o DiscardPolicy:不处理,丢弃掉。
      o 当然也可以根据应用场景需要来实现 RejectedExecutionHandler
      接口自定义策略。如记录日志或持久化不能处理的任务。

    向线程池提交任务

    我们可以使用 execute 提交的任务,但是 execute 方法没有返回值,所以无法判
    断任务是否被线程池执行成功。通过以下代码可知 execute 方法输入的任务是一
    个 Runnable 类的实例。

    threadsPool.execute(new Runnable() {
    @Override
    public void run() {
    // TODO Auto-generated method stub
    }
    });
    

    可以使用 submit 方法来提交任务,它会返回一个 future,那么我们可以
    通过这个 future 来判断任务是否执行成功,通过 future 的 get 方法来获取返回
    值, get 方法会阻塞住直到任务完成,而使用 get(long timeout, TimeUnit unit)
    方法则会阻塞一段时间后立即返回,这时有可能任务没有执行完。

    Future<Object> future = executor.submit(harReturnValuetask);
    try {
    Object s = future.get();
    } catch (InterruptedException e) {
    // 处理中断异常
    } catch (ExecutionException e) {
    // 处理无法执行任务异常
    2} finally {
    // 关闭线程池
    executor.shutdown();
    }
    

    线程池的关闭

    调用线程池的 shutdown 或 shutdownNow 方法来关闭线程池,
    它们
    的原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法来中断
    线程,所以无法响应中断的任务可能永远无法终止。

    shutdown 和 shutdownNow 的区别

    shutdownNow 首先将线程池的状态设置成 STOP,然后尝试停止所有的正在执行或
    暂停任务的线程,并返回等待执行任务的列表,而 shutdown 只是将线程池的状
    态设置成 SHUTDOWN 状态,然后中断所有没有正在执行任务的线程。

    通常调用 shutdown 来关闭线程池,表示任务执行完关闭,如果任务不一定要执行完,
    则可以调用 shutdownNow。

    线程池执行过程

    提交一个任务到线程池,首先判断基本线程池是否已满,没有满不管线程池中有没有空闲线程都创建一个新线程来执行任务, 判断工作队列是否已满,没满则将新提交的任务存储在工作队列, 满了, 判断整个线程池是否已满, 没满, 则创建一个新的工作线程来
    执行任务,满了,则交给饱和策略来处理这个任务.

    public void execute(Runnable command) {
    if (command == null)
    throw new NullPointerException();
    //如果线程数小于基本线程数,则创建线程并执行当前任务
    if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
    //如线程数大于等于基本线程数或线程创建失败,则将当前任务放到工作队
    列中。
    if (runState == RUNNING && workQueue.offer(command)) {
    if (runState != RUNNING || poolSize == 0)
    ensureQueuedTaskHandled(command);
    }
    //如果线程池不处于运行中或任务无法放入队列,并且当前线程数量小于最
    大允许的线程数量,
    则创建一个线程执行任务。
    else if (!addIfUnderMaximumPoolSize(command))
    //抛出 RejectedExecutionException 异常
    reject(command); // is shutdown or saturated
    }
    }
    

    工作线程: 线程池创建线程时,会将线程封装成工作线程 Worker,Worker 在执
    行完任务后,还会无限循环获取工作队列里的任务来执行。

    public void run() {
    try {
    Runnable task = firstTask;
    firstTask = null;
    while (task != null || (task = getTask()) != null) {
    runTask(task);
    task = null;
    }
    } finally {
    workerDone(this);
    }
    }
    

    合理的配置线程池

    任务的性质:CPU 密集型任务,IO 密集型任务和混合型任务。
    任务的优先级:高,中和低。
    任务的执行时间:长,中和短。
    任务的依赖性:是否依赖其他系统资源,如数据库连接。

    1. CPU 密集型任务, 配置尽
      可能小的线程,如配置 Ncpu+1 个线程的线程池。

    2. IO 密集型任务则由于线程并不
      是一直在执行任务,则配置尽可能多的线程,如 2*Ncpu。

    3. 混合型的任务,如果
      可以拆分,则将其拆分成一个 CPU 密集型任务和一个 IO 密集型任务,只要这两
      个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞
      吐率,如果这两个任务执行时间相差太大,则没必要进行分解。

    1. 优先级不同的任务可以使用优先级队列 PriorityBlockingQueue 来处理。它可以
      让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到
      队列里,那么优先级低的任务可能永远不能执行。

    2. 线程提交 SQL 建议使用有界队列,有界队列能增加系统的稳定性和预警能力,可以根据需要设
      大一点,比如几千.

    线程池的监控

    通过继承线程池并重写线程池的 beforeExecute,
    afterExecute 和 terminated 方法,我们可以在任务执行前,执行后和线程池关
    闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。

    相关文章

      网友评论

        本文标题:Java 线程池原理与使用

        本文链接:https://www.haomeiwen.com/subject/ctcmrctx.html