信我,ThreadPoolExecutor经典面试问题就在这~

追求适度,才能走向成功;人在顶峰,迈步就是下坡;身在低谷,抬足既是登高;弦,绷得太紧会断;人,思虑过度会疯;水至清无鱼,人至真无友,山至高无树;适度,不是中庸,而是一种明智的生活态度。

导读:本篇文章讲解 信我,ThreadPoolExecutor经典面试问题就在这~,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

目录

先来谈谈线程池

谈谈newCachedThreadPool

实现简易线程池

标准库中的ThreadPoolExecutor

线程池的线程数目如何确定?设定成几合适?(最易错!)


先来谈谈线程池

了解线程池之前,你有哪些熟知的线程池有哪些呢?

       String(字符串常量池),MySQL JDBC,数据库连接池(DataSource);通过对他们的了解,大概知道了出现池的主要目的——减少不必要的开销,提高效率;

为什么出现了线程池呢?

        线程出现的目的是因为进程太重量级了,导致创建线程或者销毁进程效率很低,而线程就是为了资源共享,新的线程复用之前的资源,就提高了效率,但是如果线程创建/销毁 的速率非常高,那么线程的创建/销毁带来的开销就是不可忽略的;

线程池的基本原理是什么?

        这就像DataSource一样,再建立连接之后,同时也会保留之前的一些连接,后面若在需要建立连接,直接从池子中取就OK,这样也就减少了重新建立连接的开销;

        造一个池子,里面创建很多线程,当再次需要执行任务的时候,就不用再创建线程了,而是直接从池子中取出一个现成的线程供使用,即使该线程完成了任务,也不销毁线程,而是继续呆在线程池里准备迎接下一个任务;

信我,ThreadPoolExecutor经典面试问题就在这~

为什么从池子中取,要比创建线程快?

        创建线程的确需要申请一点资源,但这已经很少,很快了~但是,创建线程,是要在操作系统内核中完成的,涉及到用户态内核态的切换操作,这个操作需要一定的开销;应用程序创建线程的是需要通过系统调用来完成的,进入操作系统内核中执行,也就是说,线程本质上就是PCB,是内核中的数据结构;

        所以这个过程大概是这样的:引用程序发起创建线程的行为,内核接到指令,在内核中完成PCB的创建,再把PCB加入调度队列中,最后返回给应用程序;(如下图)

信我,ThreadPoolExecutor经典面试问题就在这~

        所以创建线程,是在内核中完成的,需要经历 用户态->内核态的转变,而从线程池中取线程,把线程放回线程池,这一套操作是纯用户态的逻辑

线程池有什么优点?

  • 减少线程创建和销毁带来性能上的开销
  • 提高效率,当任务来时,可以直接使用线程,不用等待创建
  • 方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换。使用线程池可以进行统一的分配,调优和监控。

谈谈newCachedThreadPool

        在Java中,线程池的本体叫ThreadPoolExecutor,他的构造方法写起来十分麻烦,为了简化构造方法,标准库就提供了一系列工厂方法,简化使用;

什么是工厂模式?

        new的过程中,就需要调用构造方法,有时候希望能够提供多种构造实例的方法,就需要重载构造方法来实现不同版本的实例创建,但是重载要求参数个数/类型不同,就带来了一定的限制;构造方法存在一定的局限性,为了围绕局限,就引入了工厂模式

        工厂模式,将创建产品实例的权利移交工厂,我们不再通过new来创建我们所需的对象,而是通过工厂来获取我们需要的产品。降低了产品使用者与使用者之间的耦合关系;

不明白?来看看下图:

信我,ThreadPoolExecutor经典面试问题就在这~

         这里创建线程池就没有显式new,而是通过Executors这个静态方法newCaChedThreadPool来完成的;

常见用法:

        线程池的单纯使用很简单,使用submit方法,把任务提交到线程池中即可,线程池中会有线程来完成这些任务;(如下图)

信我,ThreadPoolExecutor经典面试问题就在这~


实现简易线程池

        想要模拟实现一个简单的线程池,就要先弄清线程池基本特性:一个线程池可以同时提交N个任务,对应的线程池中则有M个线程来完成这N个任务;

如何把N个任务分配给M个线程呢?

        生产者消费者模型~

        这就需要一个阻塞队列,将提交的任务都放入队列中,创建M个线程来从队列中取元素,若队列为空,就阻塞等待新任务加入队列,若队列不为空,每个线程都去取任务,然后执行,执行完当前任务后,继续取下一个…直到队列为空,继续阻塞等待;

代码如下:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
//线程池
class MyThreadPool {
    //阻塞队列存放任务
    BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
    //存放任务
    public void submit(Runnable task) throws InterruptedException {
        queue.put(task);
    }
    //创建vip个线程来执行任务
    public MyThreadPool(int vip) {
        for(int i = 0; i < vip; i++){
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    while(true){
                        try {
                            Runnable task = queue.take();
                            task.run();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            });
            thread.start();
        }
    }
}


标准库中的ThreadPoolExecutor

        在Java的标准库中提供的ThreadPoolExecutor要复杂的多,构造方法可以支持很多参数,可以支持很多选项,来创造出不同风格的线程池;

        线程池的主要参数:

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
 TimeUnit unit, BlockingQueue<Runnable> workQueue, 
ThreadFactory threadFactory, RejectedExecutionHandler handler)

        注意:黄字为比方,更方便理解

参数 解释
corePoolSize

线程池中的核心线程数。当有请求任务来之后,若线程池已创建的线程数小于corePoolSize,会通过创建一个新线程来执行该任务。当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。

        比方一个公司的正式员工,即使摸鱼也不会被开除的那种(主要线程空闲了也不会被销毁)

maximumPoolSize

线程池允许的最大线程个数;当队列满了,并且创建的线程数小于maximumPoolSize,则线程池会创建新的线程来完成任务;

        比方一个公司的实习员工,摸鱼被发现了,就会被开除(非主要线程,空闲时间达到一定时间,就会被销毁)

keepAliveTime

空闲线程的存活时间。当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。

        比方一个公司的实习员工的最大摸鱼时间,相当于给实习生摸鱼的一个时间上线;

unit keepAIiveTime的单位。
workQueue

任务队列。用于传输和保存等待执行任务的阻塞队列;

(可以手动给线程池传入一个任务队列,线程池本来也有自己的任务队列,如果不传,也会自己内部创建)

threadFactory 线程工厂。用于创建新线程。描述了线程是如何创建的,工厂对象就负责创建线程,程序员可以手动指定线程创建策略
handler(重点) 线程拒绝策略。当线程池的线程和队列都满了(工作线程忙不过来了),若有人继续往里面添加新任务,就会执行该策略

拒绝策略:(重点)注意:括号内为比方,更方便理解

    • static class  ThreadPoolExecutor.AbortPolicy

      被拒绝的任务的处理程序,抛出一个 RejectedExecutionException

              (好比领导给员工安排了很多活,加班都干不完,这个员工干不下去了,一瞬间就情绪崩溃,晕倒了;)

      static class  ThreadPoolExecutor.CallerRunsPolicy

      一个被拒绝的任务的处理程序,直接在 execute方法的调用线程中运行被拒绝的任务,除非执行程序已经被关闭,否则这个任务被丢弃。

             ( 好比领导给员工安排了很多任务,员工干不过来,员工就给领导说,你来干吧,这时候领导自己干,如果能干他就干了,不能干,就丢弃这个任务了;)

      static class  ThreadPoolExecutor.DiscardOldestPolicy

      被拒绝的任务的处理程序,丢弃最旧的未处理请求,然后重试 execute ,除非执行程序关闭,在这种情况下,任务被丢弃。

              (好比领导个员工安排了任务1、任务2、任务3,领导安排到任务4的时候,员工说谈干不了,领导就说,没事,任务1不着急,你先干任务4;)

      static class  ThreadPoolExecutor.DiscardPolicy

      被拒绝的任务的处理程序静默地丢弃被拒绝的任务。

               (好比领导个员工安排了任务1、任务2、任务3,领导安排到任务4的时候,员工说谈干不了,领导说,没事,任务4不着急,你先干之前的吧;)


线程池的线程数目如何确定?设定成几合适?(最易错!)

        这里是不能给出具体个数的,这里面试官考察的关键是——如何设置线程数目的方法(实验+测试

        线程池的线程数目无法确定具体数目,为什么呢?

  1. 主机的CPU的配置不确定
  2. 你的程序执行特点不确定

        你的代码里具体干了什么?是CPU密集型的任务(做了大量的算数运算和逻辑运算),还是IO密集型的任务(做了大量的读写网卡或者读写硬盘),还有的程序既需要进行很多的CPU密集型任务,又需要进行很多的IO任务,所以在实际的开发中,很难量化两种任务的比例;

        面临开发问题如何解决:如果任务全都是CPU密集型,线程数目最多也就是N,再多也没有意义,因为CPU已经占满了;但如果任务中10%是CPU密集型,90%是IO,线程数目设置成10N也没有关系;

        测试方法:实际开发处理方案是需要实验验证的!针对你的程序性能测试,分别给线程池设置成不同的数目:0.5N、N、1.5N、2N…都可以试试,然后分别记录每种情况下你的程序的一些核心性能指标和系统负载情况,最后选择一个合适的配置


信我,ThreadPoolExecutor经典面试问题就在这~

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/129857.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!