Java多线程之线程池面试常见问题

为什么要使用线程池

线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。

它的主要特点为:线程复用;控制最大并发数;管理线程。

  • 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度:当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性: 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池的工作原理

  1. 当线程池中的线程数量小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程;
  2. 当线程池中的线程数量达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行;
  3. 当workQueue已满,且MaximumPoolSize>CorePoolSize时,新提交的任务会创建新的非核心线程执行任务;
  4. 当并且队列已满,并且提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理;
  5. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  6. 当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲的非核心线程,最终线程池中的线程数量会收缩到corePoolSize的大小.
  7. 当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭。

注意:线程池将任务存入工作队列的时候调用的是BlockQueue的非阻塞方法offer(E e),因为工作队列满并不会使提交任务的客户端线程暂停。

常用的线程池类

  • newFixedThreadPool(int nThreads):固定线程池

创建固定数目线程的线程池,当有一个新的任务提交时,线程池中若有空闲线程,则立即执行,若没有,则新的任务会被暂存在一个任务队列(默认无界队列int最大数)中,待有线程空闲时,便处理在任务队列中的任务。

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

  • newCachedThreadPool():缓存线程池

创建一个可缓存的线程池,若有空闲线程可以复用,则会优先使用可复用的线程,如果所有线程均在工作,此时有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。因为corePoolSize为0,因此空闲线程会在指定时间内(60秒)被回收。

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

newSingleThreadExecutor():单例线程池

创建一个单线程化的Executor,只会用唯一的工作线程来执行任务,包含一个无界队列,若多余一个任务被提交到该线程池,任务会被保存在一个任务队列(默认无界队列 int 最大数)中,待线程空闲,按先入先出的顺序执行队列中的任务。

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

newScheduledThreadPool(int corePoolSize):定时线程池

创建一个支持定时及周期性任务执行的定时线程池.

扫描二维码关注公众号,回复: 8620841 查看本文章
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, 
          Integer.MAX_VALUE, 
          0, 
          NANOSECONDS,
          new DelayedWorkQueue());
}

FixedThreadPool 和 SingleThreadExecutor:允许请求的队列长度为Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。

CachedThreadPool 和 ScheduledThreadPool:允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量线程,从而导致OOM。

线程池的核心参数

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
  1. corePoolSize(核心线程数):当提交一个任务到线程池时,如果当前 poolSize < corePoolSize 时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程,这样可以减少任务被线程池处理时所需的等待时间。
  2. maximumPoolSize(最大线程数):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没什么效果。
  3. keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以,如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。
  4. TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微秒)。
  5. workQueue(任务队列):用于保存等待执行的任务的阻塞队列,它相当于生产者-消费者模式中的传输通道。
  6. threadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
  7. RejectExecutionHandler(饱和策略):队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。

线程池的拒绝策略

在 JDK1.5 中 Java 线程池框架提供了以下 4 种策略:

  1. AbortPolicy:直接抛出RejectedExecutionException异常阻止系统正常运行。
  2. CallerRunsPolicy:使用调用者所在线程来运行任务。该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量.
  3. DiscardOldestPolicy:将工作队列中最老的任务丢弃,然后重新尝试接纳被拒绝的任务。
  4. DiscardPolicy:丢弃当前被拒绝的任务,不抛出任何异常。

当然,也可以根据应用场景需要来实现RejectedExecutionHandler 接口自定义策略。如记录日志或持久化存储不能处理的任务。

线程池中的常用任务队列

可以选择以下几个阻塞队列:

  • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
  • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于 ArrayBlockingQueue。静态工厂方法固定线程池和单例线程池使用了这个队列。
  • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool 使用了这个队列,使用这个队列向线程池提交的任务不会被真是地保存,而总是将新任务提交给线程执行,如果没有空闲的线程,则尝试创建新的线程。
  • PriorityBlockingQueue:一个具有优先级的阻塞队列。该队列可以控制任务的执行先后顺序,其他队列都是按照先进先出算法处理任务的,而PriorityBlockingQueue则根据任务自身的优先级顺序先后执行。

如何终止线程池

shutdown:当线程池调用该方法时,线程池的状态立刻变为SHUTDOWN状态,已提交的任务会被继续执行,而新提交的任务会像线程池饱和时那样被拒绝掉。

shutdownNow:执行该方法,拒绝接收新提交的任务,(1)线程池的状态立即变为STOP,(2)并试图阻止所有正在执行的线程,(3)不再处理还在线程池队列中等待的任务,当然,它会返回那些已提交但是未被执行的任务列表。

shutdownNow内部是通过调用工作者线程的interrupt方法来停止正在执行的任务的,但是这种方法的作用有限,如果线程中没有sleep、wait、Condition、定时锁等应用,interrupt是无法中断当前线程的。所以,shutdownNow并不代表线程池一定会立刻退出,它可能需要等待所有正在执行的任务都执行完毕才会退出。反过来说,在关闭线程池的时候如果我们能够确保已经提交的任务都已执行完毕并且没有新的任务会被提交,那么调用shutdownNow总是安全可靠的。

只要调用了这两个关闭方法的其中一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。

至于我们应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow

最大的区别是:shutdown是会执行完所有的任务,shutdownnow是可能会执行完正在执行的任务,但是一定会抛弃等待中的任务.

高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?

(1) 高并发、任务执行时间短的业务(CPU密集型),线程池线程数可以设置为CPU核数+1,减少线程上下文的切换

(2) 并发不高、任务执行时间长的业务要区分开看:

a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务,一般设置为2*Cpu核数.

b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换.

(3) 并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。

我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。

执行execute()方法和submit()方法的区别是什么呢?

  • execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;

  • submit()方法用于提交需要返回值的任务。线程池会返回一个Future类型的对象,通过这个Future对象可以判断任务是否执行成功,并且可以通过Future的get()方法来获取返回值.

注意:Future的get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

客户端代码应该尽可能早地向线程池提交任务,并仅在需要相应任务的处理结果数据的那一刻才调用Future.get().

如果你提交任务时,线程池队列已满,这时会发生什么

如果使用的是无界队列LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务.

如果使用的是有界队列比如ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,会根据maximumPoolSize的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue继续满,那么则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy.

线程池使用注意事项

同一个线程池只能用于执行相互独立的任务。彼此有依赖关系的任务需要提交给不同的线程池执行以避免死锁。

发布了24 篇原创文章 · 获赞 8 · 访问量 929

猜你喜欢

转载自blog.csdn.net/kaihuishang666/article/details/103876358