Java教程

2021秋招最新JAVA面试题|线程池相关题目

本文主要是介绍2021秋招最新JAVA面试题|线程池相关题目,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

JAVA基础篇面试题

文章目录

      • JAVA基础篇面试题
        • 1. Callable接口详解
        • 2. 介绍下线程池
        • 3. 常见的3种线程池
        • 4. 线程池的拒绝策略
        • 5. 线程池的配置
        • 6. 死锁的概念与产生死锁的主要原因
        • 7. 死锁产生的必要条件与解决死锁的方法
        • 8. 死锁检测方法

1. Callable接口详解

作用:Callable接口详解

优势

  • 可以获得任务执行返回值;
  • 通过与Future的结合,可以实现利用Future来跟踪异步计算的结果。

Runnable与Callable的区别

  • Callable规定的方法是call(),Runnable规定的方法是run().
  • Callable的任务执行后可返回值,而Runnable的任务是不能返回值得
  • call方法可以抛出异常,run方法不可以
  • 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

2种执行方式:

  • 借助FutureTask执行
    • FutureTask类同时实现了两个接口,Future和Runnable接口,所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
  • 借助线程池来运行

2. 介绍下线程池

概念:控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,那么将进入阻塞队列中等候。

特点:线程复用,控制最大并发数,管理线程;

优势

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

底层实现ThreadPoolExecutor使用int的高3位表示线程池状态,低29位表示线程数量。

状态名高三位接收新任务处理阻塞队列任务备注
RUNNING111YY
SHUTDOWN000NY不会接收新任务,但会处理阻塞队列剩余任务
STOP001NN会中断正在执行的任务,并抛弃阻塞队列任务
TIDYING010--所有任务执行完毕,活动线程为0即将进入终结
TERMINATED011--终结状态

3. 常见的3种线程池

newFixedThreadPool: 核心线程数=最大线程数,无救急线程。 暴露threadFactory,可以对线程进行创建与修改;

newCacheThreadPool: 核心线程数是0,最大线程数是int最大,意味着全部都是救急线程。救急线程的生命周期是60s。救急线程使用完便会销毁;

newSingleThreadExecutor: 仅有一个核心线程,返回FinalizableDelegatedExecutorService 暴露ThreadPoolExecutor ;

注意:阿里巴巴的开发手册上写到,不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险;

Executors返回的线程池对象弊端如下:

  1. FixedThreadPool和SingleThreadPool:

    允许的请求队列长度为整形最大值,可能会堆积大量的请求导致OOM;

  2. CachedThreadPool和ScheuledThreadPool:

    允许创建线程数量为整形最大值,可能会创大量的线程,导致OOM;

使用接口

/**
     * @param corePoolSize 核心线程数量
     * @param maximumPoolSize 线程池允许的最大线程数量
     * @param keepAliveTime  救急线程的空闲时间,救急线程一直空闲则回收,当阻塞队列满了,corePoolSize<maximumPoolSize时启用救急线程。
     * @param unit 针对救急线程的时间单位
     * @param workQueue 阻塞队列
     * @param threadFactory 创建线程的工厂
     * @param handler 拒绝策略 默认:new AbortPolicy()
     */
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

底层实现

  • newFixedThreadPool
// nThreads核心线程数 threadFactory 线程工厂
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
}

// 业务实现
ExecutorService threadPool = Executors.newFixedThreadPool(5, new ThreadFactory(){
            AtomicInteger i = new AtomicInteger(0);
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(() -> {r.run();});
                t.setName("Hello"+i.incrementAndGet());
                return t;
            }
        });

核心线程数=最大线程数,无救急线程。 暴露threadFactory,可以对线程进行创建与修改

  • newCacheThreadPool
//threadFactory 线程工厂
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);

核心线程数是0,最大线程数是int最大,意味着全部都是救急线程。救急线程的生命周期是60s。救急线程使用完便会销毁,使用的是SynchronousQueue

  • newSingleThreadExecutor
//threadFactory 线程工厂
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }

仅有一个核心线程,返回FinalizableDelegatedExecutorService 暴露ThreadPoolExecutor

线程池的工作原理

  1. 在创建了线程池后,等待执行任务;
  2. 调用execute()添加新的请求任务时:
    1. 如果当前运行的线程数<核心线程数,将会创建线程执行任务;
    2. 当正在运行的线程数>=核心线程数,将任务放入阻塞队列中;
    3. 当队列满了,且运行线程数<线程池最大数量,创建救急线程执行任务;
    4. 当队列满了,且运行线程数已满,线程池启动拒绝策略;
  3. 当一个线程完成任务时,他会从队列中取下一个任务来执行;
  4. 当一个线程空闲状态,且超过了设置的时间,线程池会判断当前运行的线程数>核心线程数,则线程被回收,线程池所有任务完成后,它最终收缩到核心线程数大小。

4. 线程池的拒绝策略

  1. AbortPolicy: 让调用者抛出RejectedExecutionException异常<默认>

  2. CallerRunsPolicy:让调用者运行任务

  3. DiscardPolicy:放弃本次任务

  4. DiscardOldestPolicy: 放弃队列中最早的任务,本任务取而代之

    AbortPolicy CallerRunsPolicy DiscardPolicy DiscardOldestPolicy RejectedExecutionHandler

5. 线程池的配置

分两种情况,CPU密集型IO密集型

CPU密集型:该任务需要大量的运算,没有阻塞,CPU一直全速运行,CPU高。

CPU密集任务只有在真正的多核CPU上才可能得到加速(多线程),而在单核CPU上,无论设置几个模拟的多线程任务都无法得到加速,因为CPU总的运算能力就单核的能力。

CPU密集型任务配置尽可能少的线程数量:CPU核数+1个线程的线程池;

IO密集型1:任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2;

IO密集型2:任务需要大量的IO,即大量的阻塞;在单线程上运行IO密集型的任务会导致大量的CPU运算浪费在等待。所以在IO密集型任务中使用多线程可以大大加速程序运行,即使在单核CPU上,这种加速主要是利用了被浪费掉的阻塞时间。

IO密集型时,大量线程被阻塞,配置线程数公式:

CPU核数/(1-阻塞系数) 其中阻塞系数在0.8~0.9之间

比如8核CPU: 8/(1-0.9)=80个线程数

6. 死锁的概念与产生死锁的主要原因

概念:死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那将一直阻塞,如果系统资源充足,进程的资源请求都能得到满足,死锁出现的可能性会很低,否则就会因争夺有限的资源而陷入死锁.

**死锁产生的原因:**1.系统资源不足;2.进程运行的顺序异常;3.资源分配不当;

7. 死锁产生的必要条件与解决死锁的方法

必要条件:

  • 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
  • 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
  • 环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。

解决死锁的方法

  1. 预防死锁:

    • 资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
    • 只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
    • 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
    • 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)
  2. 避免死锁:

    预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得 较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全的状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法。
    银行家算法:首先需要定义状态和安全状态的概念。系统的状态是当前给进程分配的资源情况。因此,状态包含两个向量Resource(系统中每种资源的总量)和Available(未分配给进程的每种资源的总量)及两个矩阵Claim(表示进程对资源的需求)和Allocation(表示当前分配给进程的资源)。安全状态是指至少有一个资源分配序列不会导致死锁。当进程请求一组资源时,假设同意该请求,从而改变了系统的状态,然后确定其结果是否还处于安全状态。如果是,同意这个请求;如果不是,阻塞该进程知道同意该请求后系统状态仍然是安全的。

  3. 检测死锁:

    1. 首先为每个进程和每个资源指定一个唯一的号码;
    2. 然后建立资源分配表和进程等待表。
  4. 解除死锁:

    当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有:

    1. 剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
    2. 撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。

8. 死锁检测方法

  1. 查找进程的pid

    ps aux | grep 参数 或者 pgrep -lf 参数 或者 jps 参数

  2. jstack pid

  3. 或者用JProfiler(升级版的 JConsole )

这篇关于2021秋招最新JAVA面试题|线程池相关题目的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!