线程池是面试的热点,常见的面试问题如下:

1、池化思想,还知道哪些池化技术?

  程序的运行本质上来说就是占用系统的资源,频繁创建和销毁对象十分占用系统资源,因此使用池化技术优化资源的使用。池化技术我们接触的很多,比如数据库连接池, HTTP 连接池,Redis 连接池等。池化技术的思想: 核心思想是空间换时间,期望使用预先创建好的对象来减少频繁创建对象的性能开销,同时还可以对对象进行统一管理,减少对象使用成本。池化思想最大的作用是支持复用,避免出现空间过度使用出现内存泄露或者频繁垃圾回收等问题。

线程池实现原理:每一个 Thread 的类都有一个 start 方法。 当调用 start 启动线程时 Java 虚拟机会调用该类的 run 方法。 那么该类的 run() 方法中就是调用了 Runnable 对象的 run() 方法。 我们可以继承重写 Thread 类,在其 start 方法中添加不断循环调用传递过来的 Runnable 对象。 这就是线程池的实现原理。循环方法中不断获取 Runnable 是用 Queue 实现的,在获取下一个 Runnable 之前可以是阻塞的。

2、线程池四大方法、七大参数、四种拒绝策略?

四大方法(newFixedThreadPool、newSingleThreadExecutor、newCachedThreadPool、newScheduledThreadPool)

  • newCachedThreadPool:用来创建一个可以无限扩大的线程池,调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)
    不足:这种方式虽然可以根据业务场景自动的扩展线程数来处理我们的业务,但是最多需要多少个线程同时处理却是我们无法控制的;
    优点:如果当第二个任务开始,第一个任务已经执行结束,那么第二个任务会复用第一个任务创建的线程,并不会重新创建新的线程,提高了线程的复用率;
  • newFixedThreadPool:创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多)
    优点:newFixedThreadPool的线程数是可以进行控制的,因此我们可以通过控制最大线程来使我们的服务器达到最大的使用率,同时又可以保证即使流量突然增大也不会占用服务器过多的资源。
  • newSingleThreadExecutor:创建一个单线程的线程池,且线程的存活时间是无限的;当该线程正繁忙时,对于新任务会进入阻塞队列中(无界的阻塞队列),适用于需要保证顺序执行各个任务。
  • newScheduledThreadPool:创建一个固定大小的线程池,线程池内线程存活时间无限制,线程池可以支持定时及周期性任务执行,如果所有线程均处于繁忙状态,对于新任务会进入DelayedWorkQueue队列中,这是一种按照超时时间排序的队列结构。

七大参数

看一下源码中的定义:

java 线程池面试题 答案 java线程池应用情景面试_面试


1、corePoolSize(线程池基本大小):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,空闲时间达到keepAliveTime时,关闭空闲线程。(除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。)

2、maximumPoolSize(线程池最大大小):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。

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

4、unit(时间单位):线程存活保持时间keepAliveTime的单位。
5、workQueue(任务队列):用于传输和保存等待执行任务的阻塞队列。

6、threadFactory(线程工厂):用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。这个参数一般采用默认的就可以了。

7、handler(线程拒绝策略):当线程池和队列都满了,再加入线程会执行此策略,默认是ThreadPoolExecutor.CallerRunsPolicy

定义例子:

int processors = Runtime.getRuntime().availableProcessors();//获取CPU核数
ThreadPoolExecutor executor=new ThreadPoolExecutor(3,processors,1L, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3),Executors.defaultThreadFactory(),new ThreadPoolExecutor.CallerRunsPolicy());

四种拒绝策略

  当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize时,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。

ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。

ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务 。ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务。

JDK1.8文档描述:

java 线程池面试题 答案 java线程池应用情景面试_java 线程池面试题 答案_02

3、你平时怎么使用线程池/如何配置线程池?

CPU密集型任务
  尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。

IO密集型任务
  可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。

混合型任务
  可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。
如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。
当然,这只是提供一种思路而已,并不是所有系统使用线程池都采用这种方案。具体的参数我们需要根据系统的负载情况进行取舍。参数的设置跟系统的负载有直接的关系,下面为系统负载的相关参数:

  • tasks,每秒需要处理的最大任务数量。
  • tasktime,处理单个任务所需要的时间。
  • responsetime,系统允许任务最大的响应时间。

4、使用线程池的优势?

(1)降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
(2)提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
(3)方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
(4)提供更强大的功能,延时定时线程池。

5、线程池工作流程?

java 线程池面试题 答案 java线程池应用情景面试_线程池_03


(1)、判断线程池中当前线程数是否大于核心线程数,如果小于,在创建一个新的线程来执行任务,已满则执行第二步。

(2)、判断任务(阻塞)队列是否已满,没满则将新提交的任务添加在工作队列,已满则执行第三步。

(3)、判断整个线程池是否已满,没满则创建一个新的工作线程来执行任务,已满则执行饱和策略。

详细版:

  1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
  2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
    a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
    b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
    c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
    d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会执行拒绝策列。
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  4. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

6、线程池为什么需要使用(阻塞)队列?

一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务。阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活、不至于一直占用cpu资源。

总结起来就是如下两点:

  • 线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换。
  • 创建线程池的消耗较高。(线程池创建线程需要获取mainlock这个全局锁,影响并发效率,阻塞队列可以很好的缓冲。)

为什么不使用非阻塞队列:
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。使得在线程不至于一直占用cpu资源。

(线程执行完任务后通过循环再次从任务队列中取出任务进行执行,代码片段如下

while (task != null || (task = getTask()) != null) {})

不用阻塞队列也是可以的,不过实现起来比较麻烦而已。

7、核心线程满了后,再有任务需要执行,线程池为什么不继续创建新的线程呢,而是中间引入阻塞队列,能不能在核心线程满了后,继续创建线程,直到线程数达到最大线程数了,再把任务引入阻塞队列,这样的步骤不也是可以的?

这主要是因为在创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率就好比一个饭店里面有10个(core)正式工的名额,最多招10个正式工,要是任务超过正式工人数(task>core)的情况下,工厂领导(线程池)不是首先扩招工人,还是这10人,但是任务可以稍微积压一下,即先放到队列去(代价低) 。10个正式工慢慢干,迟早会千完的,要是任务还在继续增加,超过正式工的加班忍耐极限了(队列满了) ,就的招外包帮忙了(注意是临时工)要是正式工加上外包还是不能完成任务,那新来的任务就会被领导拒绝了(线程池的拒绝策略)

  • 线程池创建线程需要获取mainlock这个全局锁,会影响并发效率,所以使用阻塞队列把第一步创建核心线程与第三步创建最大线程隔离开来,起一个缓冲的作用。
  • 引入阻塞队列,是为了在执行execute()方法时,尽可能的避免获取全局锁。

8、核心线程数和最大线程数

一般处理器,都是几个核心,那就有几个线程。但是由于处理器技术发展越来越快,就诞生了一个“超线程”技术,该技术普遍存在inter处理器。如果是一核心一线程的话,那么核心在工作时,难免会有空闲、休息的时候。但是我们不想让它(核心)休息,就可以用超线程技术,给核心更多的线程,让处理器的利用率更高,从而提升处理器的能力。例如I3-2100就是双核心四线程,一个核心对应两条工作线,这样处理器核心绝对不会有空闲的时间。

9、线程池中线程复用原理

线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过Thread创建线程时的一个线程必须对应一个任务的限制。在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对Thread进行了封装,并不是每次执行任务都会调用Thread.start()来创建新线程,而是让每个线程去执行一个"循环任务",在这个"循环任务"中不停检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的run方法,将run方法当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的run方法串联起来。

10、execute()和submit()方法

1、execute()执行一个任务,没有返回值
2、submit()提交一个线程任务,有返回值
submit(Callable<T> task)能获取到它的返回值,通过Future.get()获取(阻塞直到任务执行完)。一般使用FutureTask+Callable配合使用。

submit(Runnable task, T result)能通过传入的载体result间接获得线程的返回值。
submit(Runnable task)则是没有返回值的,就算获取它的返回值也是null。

Future.get方法会使取结果的线程进入阻塞状态,直到线程执行完成之后,唤醒取结果的线程,然后返回结果。

void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
 <T> Future <T> submit(Callable<T>task):执行任务,有返回值,一般又用来执行Callable

使用案例:

public class ThreadPool {
    public static void main(String[] args) {
        ThreadPool threadPool=new ThreadPool();
        List<ThreadPool> lists=new ArrayList<>();
        int processors = Runtime.getRuntime().availableProcessors();//获取CPU核数
        //方法一、直接创建线程池(自定义创建线程池)
        ThreadPoolExecutor executor=new ThreadPoolExecutor(3,processors,1L, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3),Executors.defaultThreadFactory(),new ThreadPoolExecutor.CallerRunsPolicy());
        try{

            for (int i = 0; i < 8; i++) {
                executor.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"线程办理业务!");
                });
            }
        }catch (Exception ex){
            ex.printStackTrace();
        }finally {
            executor.shutdown();  //finally中保证一定会关闭
        }

        //方法二、借助工具类(不推荐、不安全,推荐使用原生的,了解其中参数)
        //1、使用工具类Executors 创建服务,创建线程池,参数为线程池大小
        ExecutorService executorService= Executors.newFixedThreadPool(10);//创建一个固定大小的线程池
//        ExecutorService executorService1= Executors.newSingleThreadExecutor(); //创建单个线程
//        ExecutorService executorService2= Executors.newCachedThreadPool(); //可伸缩的
        //2、执行任务
        executorService.execute(new MyThread());
        executorService.execute(new MyThread());
        executorService.execute(new MyThread());
        executorService.execute(new MyThread());
        //3、关闭线程池连接
        executorService.shutdown();

        //获取CPU的核数
        System.out.println("CPU核数:"+Runtime.getRuntime().availableProcessors());
    }
    static class MyThread implements Runnable{

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName());
        }
    }
}