Java面试-线程池

面试题:讲一下线程池(腾讯、京东面试题)

一、为什么使用线程池
由于创建和销毁线程都需要很大的开销,运用线程池就可以大大的缓解这些内存开销很大的问题;可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存。
二、线程池的处理流程
创建线程池需要使用 ThreadPoolExecutor 类,它的构造函数参数如下:

public ThreadPoolExecutor(
        int corePoolSize,       //核心线程的数量
        int maximumPoolSize,    //最大线程数量(核心线程+核心意以外的线程)
        long keepAliveTime,     //超出核心线程数量以外的线程空闲时的存活时间
        TimeUnit unit,          //存活时间的单位
        BlockingQueue<Runnable> workQueue,    //存放来不及处理的任务的队列,是一个BlockingQueue。
        ThreadFactory threadFactory,     //生产线程的工厂类,可以定义线程名,优先级等。
        RejectedExecutionHandler handler // 当任务无法执行时的处理器
        ) {...}

线程池执行的具体方法:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    //1.当前池中线程比核心数少,新建一个线程执行任务
    if (workerCountOf(c) < corePoolSize) {   
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    //2.核心池已满,但任务队列未满,添加到队列中
    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);
    }
    //3.核心池已满,队列已满,试着创建一个新线程
    else if (!addWorker(command, false))
        reject(command);    //如果创建新线程失败了,说明线程池被关闭或者线程池完全满了,拒绝任务
}

线程比作员工,线程池比作一个团队
1、有了新需求,先看核心员工数量超没超出最大核心员工数,还有名额的话就新招一个核心员工来做
2、核心员工已经最多了,放到待完成任务列表吧
3、如果列表已经堆满了,核心员工基本没机会搞完这么多任务了,那就找个外包吧
4、如果核心员工 + 外包员工的数量已经是团队最多能承受人数了,没办法,这个需求接不了了,交给handle处理器处理
handler处理策略:
1、CallerRunsPolicy:只要线程池没关闭,就直接用调用者所在线程来运行任务
2、AbortPolicy:直接抛出 RejectedExecutionException 异常
3、DiscardPolicy:悄悄把任务放生,不做了
4、DiscardOldestPolicy:把队列里待最久的那个任务扔了,然后再调用 execute() 试试看能行不
我们也可以实现自己的 RejectedExecutionHandler 接口自定义策略,比如如记录日志什么的
消息队列
线程池中使用的队列是 BlockingQueue 接口,常用的实现有如下几种:
① ArrayBlockingQueue:基于数组、有界,按 FIFO(先进先出)原则对元素进行排序
② LinkedBlockingQueue:基于链表,按FIFO (先进先出) 排序元素
③ SynchronousQueue:不存储元素的阻塞队列 ,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态
④ PriorityBlockingQueue:具有优先级的、无限阻塞队列

三、JDK提供的线程池
JDK 为我们内置了五种常见线程池的实现,均可以使用 Executors 工厂类创建。
1、newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));

一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

public class newSingleThreadExecutor {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            pool.execute(() -> {
                System.out.println(Thread.currentThread().getName() + "\t开始发车啦....");
            });
        }
    }
}
//运行结果
pool-1-thread-1 开始发车啦....
pool-1-thread-1 开始发车啦....
pool-1-thread-1 开始发车啦....
pool-1-thread-1 开始发车啦....
pool-1-thread-1 开始发车啦....
pool-1-thread-1 开始发车啦....
pool-1-thread-1 开始发车啦....
pool-1-thread-1 开始发车啦....
pool-1-thread-1 开始发车啦....
pool-1-thread-1 开始发车啦....
从输出的结果我们可以看出,一直只有一个线程在运行。

2、newFixedThreadPool


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

不招外包,有固定数量核心成员的正常互联网团队。
FixedThreadPool 的核心线程数和最大线程数都是指定值,也就是说当线程池中的线程数超过核心线程数后,任务都会被放到阻塞队列中。此外 keepAliveTime 为 0,也就是多余的空闲线程会被立即终止,而这里选用的阻塞队列是 LinkedBlockingQueue,使用的是默认容量 Integer.MAX_VALUE,相当于没有上限。
执行流程:
① 线程数少于核心线程数,也就是设置的线程数时,新建线程执行任务
② 线程数等于核心线程数后,将任务加入阻塞队列,于队列容量非常大,可以一直加加加
③ 执行完任务的线程反复去队列中取任务执行
FixedThreadPool 用于负载比较重的服务器,为了资源的合理利用,需要限制当前线程数量。

public class newFixedThreadPool {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(6);
        for (int i = 0; i < 10; i++) {
            pool.execute(() -> {
                System.out.println(Thread.currentThread().getName() + "\t开始发车啦....");
            });
        }
    }
}
//运行结果
pool-1-thread-1 开始发车啦....
pool-1-thread-2 开始发车啦....
pool-1-thread-3 开始发车啦....
pool-1-thread-4 开始发车啦....
pool-1-thread-5 开始发车啦....
pool-1-thread-6 开始发车啦....
pool-1-thread-6 开始发车啦....
pool-1-thread-1 开始发车啦....
pool-1-thread-2 开始发车啦....
pool-1-thread-3 开始发车啦....

3、newCachedThreadPool


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

全部外包,没活最多待 60 秒的外包团队。
可以看到,CachedThreadPool 没有核心线程,非核心线程数无上限,也就是全部使用外包,但是每个外包空闲的时间只有 60 秒,超过后就会被回收。
CachedThreadPool 使用的队列是 SynchronousQueue,这个队列的作用就是传递任务,并不会保存。
因此当提交任务的速度大于处理任务的速度时,每次提交一个任务,就会创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。
执行流程:
① 没有核心线程,直接向 SynchronousQueue 中提交任务
② 如果有空闲线程,就去取出任务执行;如果没有空闲线程,就新建一个
③ 执行完任务的线程有 60 秒生存时间,如果在这个时间内可以接到新任务,就可以继续活下去,否则就拜拜
CachedThreadPool 用于并发执行大量短期的小任务,或者是负载较轻的服务器。
4、newScheduledThreadPool
核心和外包都有,此线程池支持定时以及周期性执行任务的需求。

public class newScheduledThreadPool {
    public static void main(String[] args) {
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(10);
        for (int i = 0; i < 2; i++) {
            //每隔一秒执行一次
            pool.scheduleAtFixedRate(() -> {
                System.out.println(Thread.currentThread().getName() + "\t开始发车啦....");
            }, 1, 1, TimeUnit.SECONDS);
        }
    }
}

四、execute和submit的区别/b>
execute():提交不需要返回值的任务
submit():提交需要返回值的任务
五、关闭线程池/b>
有两个方法关闭线程池:
shutdown()
将线程池的状态设置为 SHUTDOWN,然后中断所有没有正在执行的线程
shutdownNow()
对正在执行的任务全部发出interrupt(),停止执行,对还未开始执行的任务全部取消,并且返回还没开始的任务列表。
它们的共同点是:都是通过遍历线程池中的工作线程,逐个调用 Thread.interrup() 来中断线程,所以一些无法响应中断的任务可能永远无法停止(比如 Runnable)。
六、如何合理地选择
1、CachedThreadPool 用于并发执行大量短期的小任务,或者是负载较轻的服务器
2、FixedThreadPool 用于负载比较重的服务器,为了资源的合理利用,需要限制当前线程数量
3、SingleThreadExecutor 用于串行执行任务的场景,每个任务必须按顺序执行,不需要并发执行
4、ScheduledThreadPoolExecutor 用于需要多个后台线程执行周期任务,同时需要限制线程数量的场景
自定义线程池时,如果任务是 CPU 密集型(需要进行大量计算、处理),则应该配置尽量少的线程,比如 CPU 个数 + 1,这样可以避免出现每个线程都需要使用很长时间但是有太多线程争抢资源的情况;
如果任务是 IO密集型(主要时间都在 I/O,CPU 空闲时间比较多),则应该配置多一些线程,比如 CPU 数的两倍,这样可以更高地压榨 CPU。

转载请标明出处,原文地址:https://blog.csdn.net/weixin_41835916 如果觉得本文对您有帮助,请点击支持一下,您的支持是我写作最大的动力,谢谢。
这里写图片描述

猜你喜欢

转载自blog.csdn.net/weixin_41835916/article/details/81449641