每日三道面试题,通往自由的道路12——线程池

分享 未结 0 1129
邮箱用户_pb5l1
邮箱用户_pb5l1 LV1 2021年7月19日 23:27 编辑

茫茫人海千千万万,感谢这一秒你看到这里。希望我的面试题系列能对你的有所帮助!共勉!

愿你在未来的日子,保持热爱,奔赴山海!

每日三道面试题,成就更好自我

昨天既然聊到线程池中的实现方式,有些比较重要的我还没问到。

1. 你知道ThreadPoolExecutor的构造方法和参数吗

我们先来看看它的构造方法有哪些:

// 五个参数的构造函数
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {...}

// 六个参数的构造函数
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory) {...}

// 六个参数的构造函数
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler) {...}

// 七个参数的构造函数
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {...}

我们再来详解下构造方法中涉及的7个参数,其中最重要5个参数就是第一个构造方法中的。

  • int corePoolSize:该线程池中核心线程数量

    核心线程:线程池中有两类线程,核心线程和非核心线程。核心线程默认情况下会一直存在于线程池中,即使这个核心线程什么都不干,而非核心线程(临时工)如果长时间的闲置,就会被销毁。但是如果将

    allowCoreThreadTimeOut设置为true时,核心线程也是会被超时回收。

  • int maximumPoolSize:该线程池中允许存在的工作线程的最大数量。

    该值相当于核心线程数量 + 非核心线程数量。

  • long keepAliveTime:非核心线程闲置超时时长。

    非核心线程如果处于闲置状态超过该值,就会被销毁。如果设置allowCoreThreadTimeOut(true),则会也作用于核心线程。

  • TimeUnit unit:keepAliveTime的时间单位。

    TimeUnit是一个枚举类型 ,包括以下属性:

    NANOSECONDS : 1微毫秒 
    MICROSECONDS : 1微秒
    MILLISECONDS : 1毫秒
    SECONDS : 秒 
    MINUTES : 分
    HOURS : 小时
    DAYS : 天
  • BlockingQueue workQueue:阻塞队列,维护着等待执行的Runnable任务对象。

    当新任务来的时候,会先判断当前运行线程数量是否达到了核心线程数,如果达到了,就会被存放在阻塞队列中排队等待执行。

    常用的几个阻塞队列:

    1. ArrayBlockingQueue

      数组阻塞队列,底层数据结构是数组,需要指定队列的大小。

    2. SynchronousQueue

      同步队列,内部容量为0,每个put操作必须等待一个take操作,反之亦然。

    3. DelayQueue

      延迟队列,该队列中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素 。

    4. LinkedBlockingQueue

      链式阻塞队列,底层数据结构是链表,默认大小是Integer.MAX_VALUE,也可以指定大小。

还有两个非必须的参数:

  • ThreadFactory threadFactory

    创建线程的工厂 ,用于批量创建线程,统一在创建线程时设置一些参数,如是否守护线程、线程的优先级等。如果不指定,会新建一个默认的线程工厂。

  • RejectedExecutionHandler handler

    拒绝处理策略,在线程数量大于最大线程数后就会采用拒绝处理策略,四种拒绝处理的策略为 :

    1. ThreadPoolExecutor.AbortPolicy默认拒绝处理策略,丢弃任务并抛出RejectedExecutionException异常。
    2. ThreadPoolExecutor.DiscardPolicy:丢弃新来的任务,但是不抛出异常。
    3. ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列头部(最旧的)的任务,然后重新尝试执行程序(如果再次失败,重复此过程)。
    4. ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。

不错呀!线程池的参数也有深入了解,那咱们继续

2. 你可以说下线程池的执行过程原理吗

昨天MyGirl跟我讲了一下她去银行办理业务的一个场景:

  1. 首先MyGirl(任务A)先去银行(线程池)办理业务,她发现她来早了,现在银行才刚开门,柜台窗口服务员还没过来(相当于线程池中的初始线程为0),此时银行经理看到MyGirl来了,就安排她去一号柜台窗口并安排了1号正式工作人员来接待她。
  2. 在MyGirl的业务还没办完时,一个不知名的路人甲(任务B)出现了,他也是要来银行办业务,于是银行经理安排他去二号柜台并安排了2号正式工作人员。假设该银行的柜台窗口就只有两个(核心线程数量2)。
  3. 紧接着,在所有人业务都还没做完的情况,持续来个三个不知名的路人乙丙丁,他们也是要来办业务的,但是由于柜台满了,安排了他们去旁边的银行大厅的座位上(阻塞队列,这里假设大小为3)等候并给了对应顺序的号码,说等前面两个人办理完后,按顺序叫号你们呦,请注意听。
  4. 过一会,一个路人戊也想来银行办理业务,而经理看到柜台满了,座位满了,只能安排了一个临时工(非核心线程,这里假设最大线程为3,即非核心为1)手持pad设备并给路人戊去办理业务。
  5. 而此时,一个路人戌过来办理业务,而经理看到柜台满了,座位满了,临时工也安排满了(最大线程数+阻塞队列都满了),无奈经理只能掏出一本《如何接待超出最大限度的手册》,选择拒接接待路人戌通知他,过会再来吧您嘞,这里已经超负荷啦!
  6. 最后,相继所有人的业务都办完了,现在也没人再来办业务,并且临时工的空闲时间也超过了1小时以上了(最大空闲时间默认60秒),经理让临时工都先下班回家了(销毁线程)。
  7. 但是一个银行要保证正常的运行,只能让正式员工继续上班,不得提早下班。

而实际上线程的流程原理跟这个一样,我们来看下处理任务的核心方法execute,它的源码大概是什么样子的呢,当然我们也可以看源码中的注释,里面也写的很清楚。这里具体讲下思路。

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();  
    // 1. 获取ctl,ctl是记录着线程池状态和线程数。
    int c = ctl.get();
    // 2. 判断当前线程数小于corePoolSize核心线程,则调用addWorker创建核心线程执行任务
    if (workerCountOf(c) < corePoolSize) {
       if (addWorker(command, true))
           return;
       // 创建线程失败,需要重新获取clt的状态和线程数。
       c = ctl.get();
    }
    // 3. 如果不小于corePoolSize,进入下面的方法。
    // 判断线程池是否运行状态并且运行线程数大于corePoolSize,将任务添加到workQueue队列。
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // 3.1 再次检查线程池是否运行状态。
        // 如果isRunning返回false(状态检查),则remove这个任务,然后执行拒绝策略。
        if (! isRunning(recheck) && remove(command))
            reject(command);
            // 3.2 线程池处于running状态,但是没有线程,则创建线程加入到线程池中
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 4. 如果放入workQueue失败,则创建非核心线程执行任务,
    // 如果这时创建非核心线程失败(当前线程总数不小于maximumPoolSize时),就会执行拒绝策略。
    else if (!addWorker(command, false))
         reject(command);
}

我们可以大概看下思路图:

先解释下ctl

变量ctl定义为AtomicInteger,记录了“线程池中的任务数量”和“线程池的状态”两个信息。以高三位记录着线程池的状态和低29位记录线程池中的任务数量。

RUNNING : 111
SHUTDOWN : 000
STOP : 001
TIDYING : 010
TERMINATED : 011

最后总结一下执行过程:

  1. 任务到达时,会先判断核心线程是否满了,不满则调用addWorker方法创建核心线程执行任务。
  2. 然后会判断下线程池中的线程数 < 核心线程,无论线程是否空闲,都会新建一个核心线程执行任务(让核心线程数量快速达到核心线程总数)。此步骤会开启锁mainLock.lock();
  3. 而在线程池中的线程数 >= 核心线程时,新来的线程任务会进入任务阻塞队列中等待,然后空闲的核心线程会依次去阻塞队列中取任务来执行。
  4. 当阻塞队列满了,说明这个时候任务很多了,此时就需要一些非核心线程临时工来执行这些任务了。于是会创建非核心线程去执行这个任务。
  5. 最后当阻塞队列满了, 且总线程数达到了maximumPoolSize,则会采取拒绝策略进行处理。
  6. 当非核心线程取任务的时间达到keepAliveTime还没有取到任务即空闲时间,就会回收非核心线程。

不错,这个执行过程原理都有深入了解过,最后问你一道:

3. 能否写一个简单线程池的demo?

你这怕不是魔鬼吧,写一个线程池。不过简单的线程池还是可以写写滴!当然通过上面参数,执行过程的学习,写出来一个还是比较So Easy的。只是如果真的到面试了,真的让你手敲,可能就忘了,还是得多敲。

这里还是直接用简单的ThreadPoolExecutor创建吧,等后续写线程池相关文章,再详细写自己创建的线程池吧。

我们先创建一个任务类Task:

/**
 * 自定义任务类
 */
public class Task implements Runnable{

    private int id;

    public Task(int id) {
        this.id = id;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "即将执行的任务是" + id + "任务");
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "执行完成的任务是" + id + "任务");
    }
}

测试代码:

public class ThreadPoolExecutorDemo {

    private static final int CORE_POOL_SIZE = 3;
    private static final int MAX_POOL_SIZE = 5;
    private static final int QUEUE_CAPACITY = 10;
    private static final Long KEEP_ALIVE_TIME = 1l;


    public static void main(String[] args) {
        //通过ThreadPoolExecutor构造函数自定义参数创建
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy());

        for (int i = 0; i < 10; i++) {
            Task task = new Task( i);
            //执行Runnable
            executor.execute(task);
        }
        //终止线程池
        executor.shutdown();
        while (!executor.isTerminated()) {
        }
        System.out.println("线程已经全部执行完");
    }
}

得到的结果:

pool-1-thread-1即将执行的任务是0任务
pool-1-thread-3即将执行的任务是2任务
pool-1-thread-2即将执行的任务是1任务
pool-1-thread-1执行完成的任务是0任务
pool-1-thread-3执行完成的任务是2任务
pool-1-thread-1即将执行的任务是3任务
pool-1-thread-3即将执行的任务是4任务
pool-1-thread-2执行完成的任务是1任务
pool-1-thread-2即将执行的任务是5任务
pool-1-thread-3执行完成的任务是4任务
pool-1-thread-1执行完成的任务是3任务
pool-1-thread-3即将执行的任务是6任务
pool-1-thread-1即将执行的任务是7任务
pool-1-thread-2执行完成的任务是5任务
pool-1-thread-2即将执行的任务是8任务
pool-1-thread-3执行完成的任务是6任务
pool-1-thread-1执行完成的任务是7任务
pool-1-thread-3即将执行的任务是9任务
pool-1-thread-2执行完成的任务是8任务
pool-1-thread-3执行完成的任务是9任务
线程已经全部执行完

当然此版写的稍微简单,但是如果真的忘记,也可以这么写。如果后续想看更多东西,可以关注我呀,我会持续更新内容!

小伙子不错嘛!今天就到这里,期待你明天的到来,希望能让我继续保持惊喜!

参考资料:线程池原理

注: 如果文章有任何错误和建议,请各位大佬尽情留言!如果这篇文章对你也有所帮助,希望可爱亲切的您给个三连关注下,非常感谢啦!


收藏(0)  分享
相关标签: 面试题
问题没解决?让chatGPT帮你作答 智能助手
0个回复
  • 消灭零回复
相关帖子