Java面试总结(三)

    • 1、token 为什么存放在 redis 中?
    • 2、索引的底层原理是什么?
    • 3、Spring IOC和AOP的原理
    • 4、接口和抽象类有什么共同点和区别?
    • 5、为什么要使用线程池?直接new个线程不好吗?
    • 6、 线程池的核心属性有哪些?
    • 7、线程池中的各个状态分别代表什么含义?
    • 8、线程池有哪些队列?
    • 9、线程池有哪些拒绝策略?
    • 10、Executors 提供了哪些创建线程池的方法?

1、token 为什么存放在 redis 中?


答:Token 是一种身份验证机制,用于验证用户的身份。在Web应用程序中,通常使用token来保护用户的敏感信息和操作。将token存储在Redis可以提供以下优点:

  1. 快速访问:Redis是内存数据库,读取速度非常快。这意味着通过Redis存储的token可以快速访问,从而提高了应用程序的性能。
  2. 分布式环境下的共享:在分布式环境中,多个服务器可能需要访问同一个token。将token存储在Redis中,可以使不同的服务器共享同一个token,从而确保应用程序的一致性。
  3. 自动过期:Redis支持设置过期时间,可以让token在一段时间后自动过期。这可以防止token被滥用和保护用户的安全。
  4. 可以进行监控和管理:Redis提供了监控和管理工具,可以帮助开发人员查看token的使用情况和管理token的过期时间。

2、索引的底层原理是什么?


答:索引(index)是能够帮助数据库提升查询效率的一种数据结构。换一种易懂的说法:索引是一种数据结构,他的作用是使数据库查询数据更快。
数据库对于索引的处理过程
在我们为表的某个字段设置索引之后,数据库会将该字段数据复制,并将复制的数据以某种数据结构重新排列,保存为索引文件。当使用索引字段做查询条件对该表进行查询操作的时候,数据库会打开索引文件并通过其中的数据结构来快速定位目标数据。
索引应用的数据结构
B TREE(Balance tree)、B+TREE
B TREE和B+TREE是一种优化关系:B TREE先出现,通过对它的优化设计产生了B+TREE。
B TREE

上图为一个B TREE的结构示例,下面对B TREE中的元素逐一说明,需要结合图片理解:
1、阶:阶表示B TREE的层数(横向),那么B TREE为3阶平衡树
2、节点:每一个节点都是一个方块,位于最顶部的叫做根节点,最底部是叶子结点,其他的叫中间节点。每个节点中包含下列元素:
数字(K):关键字(存放索引字段)
Data(D):关键字对应详细数据Data
指针(P):指向下方子节点

BTree的特点:
1、叶子结点无P指针
2、指针P和关键字K交叉排序,所以每个节点中的指针P一定比关键字K多一个
3、关键字从左到右有序排列(K后的序号顺序)
4、所有子节点的关键字范围不得超过指针对应父节点左右两侧关键字的范围。
5、所有叶子节点的深度(阶数)相同
6、所有节点的关键字不重复(关键字可能不必走到叶子节点即可命中Data)
通过Btree的结构可以分析出:B TREE以类似二分的结构,通过范围过滤的算法来提高查询效率,当关键字在某节点命中后,获取该K的data来返回查询结果,这就是索引的底层原理。
B + TREE
B+TREE是基于B TREE的一种增强设计

B+TREE的特点:
1、节点中关键字x与指针p个数相同
2、目标数据只有到达叶子节点才会命中(data数据只存在于叶子节点),也就是说使用B+TREE的索引一定会走到叶子节点
3、每个叶子节点增加一个指针,按顺序指向相邻叶子节点(提升区间访问性能)
以上就是索引采用的两种底层数据结构,目前数据库产品的索引大都基于这两种数据结构,接下来以MYSQL为例看一下索引的应用实例:
MySQL索引实现
mysql中的索引是采用B+TREE实现的。另外,在mysql中有一个存储引擎的概念(可以理解为mysql存数据的一个中间处理器)。

  • MyISAM:在MyISAM存储引擎中索引的实现逻辑是:在B+TREE的叶子节点存放着对应记录的内存地址D,然后再通过该地址D关联对应表数据以获取消息数据,这种设计方式实现的索引叫做“非聚合索引”,
  • InnoDB:与MyISAM最核心的区别是:在InnoDB存储引擎中,叶子节点直接存放了数据详细信息,也就是说InnoDB直接将表数据本身做成了B+TREE数据结构(由于不需要再查一次表数据,这使得查询速度更快),这种设计方式实现的索引叫做“聚合索引”。

3、Spring IOC和AOP的原理


答:1、IOC:控制反转,程序之间解耦,在类和类之间存在控制权,控制权指的是对象的创建和使用
比如说有类A和类B,我们之前的做法是在A中调用B,那么控制权就在A中,这样做的耦合度高,如果修改了B,A也要做相应修改。引入Spring框架后,控制权由Spring容器来负责。当A想使用B时,需要由Spring容器通过配置文件进行注入。这种思想就是ioc(将对象的创建和使用控制权转移到了Spring容器,由Spring容器来控制)。
2、AOP:面向切面编程,Struts2的拦截器,就是使用AOP的思想。使用AOP来管理事务(事务处理、日志管理、权限控制等)。Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接口,那么AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用JDK proxy去进行代理,而是会使用Cglib Proxy来生成一个被代理对象的子类作为代理。

4、接口和抽象类有什么共同点和区别?


答:

  • 共同点:
    • 都不能被实例化。
    • 都可以包含抽象方法。
    • 都可以有默认实现的方法(Java 8 可以用 default 关键字在接口中定义默认方法)。
  • 区别:
    • 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
    • 一个类只能继承一个类,但是可以实现多个接口。
    • 接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。

5、为什么要使用线程池?直接new个线程不好吗?


答:如果我们在方法中直接new一个线程来处理,当这个方法被调用频繁时会创建很多线程,不仅会消耗系统资源,还会降低系统的稳定性。
使用线程池的好处:

  1. 降低资源消耗。通过重复利用已创建的线程,降低线程创建和销毁造成的消耗。
  2. 提高响应速度。当任务到达时,任务可以不需要等到线程床就能立即执行。
  3. 增加线程的可管理性。线程是稀缺资源,使用线程池可以进行统一分配,调优和监控。

6、 线程池的核心属性有哪些?


答:

  • threadFactory(线程工厂):用于创建工作线程的工厂。
  • corePoolSize(核心线程数):当线程池运行的线程少于- corePoolSize 时,将创建一个新线程来处理请求,即使其他工作线程处于空闲状态。
  • workQueue(队列):用于保留任务并移交给工作线程的阻塞队列。
  • maximumPoolSize(最大线程数):线程池允许开启的最大线程数。
  • handler(拒绝策略):往线程池添加任务时,将在下面两种情况出发拒绝策略:1)线程池运行状态不是RUNNING;2:)线程池已经达到最大线程数,并且阻塞队列已满时。
  • keepAliveTime(保持存活时间):如果线程池当前线程数超过 corePoolSize,则多余的线程空闲时间超过 keepAliveTime 时会被终止。

7、线程池中的各个状态分别代表什么含义?


答:线程池目前有5个状态:

  • RUNNING:接受新任务并处理排队的任务。
  • SHUTDOWN:不接受新任务,但处理排队的任务。
  • STOP:不接受新任务,不处理排队的任务,并中断正在进行的任务。
  • TIDYING:所有任务都已终止,workerCount为零,线程转换到 TIDYING 状态将运行 terminated() 钩子方法。
  • TERMINATED:terminated() 已完成。

8、线程池有哪些队列?


答:常见的阻塞队列有以下几种:

  • ArrayBlockingQueue: 基于数组结构的有界阻塞队列,按先进先出对元素进行排序。

  • LinkedBlockingQueue:基于链表结构的有界/无解阻塞队列,按先进先出对元素进行排序,吞吐量通常高于 ArrayBlockingQueue。Executor.newFixedThreadPool 使用了该队列。

  • SynchronousQueue:不是一个真正的队列,而是一种在线程之间移交的机制。要将一个元素放入 SynchronousQueue 中,必须有另一个线程正在等待接受这个元素。如果没有线程等待,并且线程池的当前大小小于最大值,那么线程池将创建一个线程,否则根据拒绝策略,这个任务将被拒绝。使用直接移交将更高效,因为任务会直接移交给执行它的线程,而不是被放在队列中,然后由工作线程从队列中提取任务。只有当线程池是无界的或者可以拒绝任务时,该队列才有实际价值。Executor.newCachedThreadPool使用了该队列。

  • PriorityBlockingQueue:具有优先级的无界队列,按优先级对元素进行排序。元素的优先级是通过自然顺序或 Comparator 来定义的。

9、线程池有哪些拒绝策略?


答:常见的有以下几种:

AbortPolicy:中止策略。默认的拒绝策略,直接抛出 RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。

DiscardPolicy:抛弃策略。什么都不做,直接抛弃被拒绝的任务。

DiscardOldestPolicy:抛弃最老策略。抛弃阻塞队列中最老的任务,相当于就是队列中下一个将要被执行的任务,然后重新提交被拒绝的任务。如果阻塞队列是一个优先队列,那么“抛弃最旧的”策略将导致抛弃优先级最高的任务,因此最好不要将该策略和优先级队列放在一起使用。

CallerRunsPolicy:调用者运行策略。在调用者线程中执行该任务。该策略实现了一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将任务回退到调用者(调用线程池执行任务的主线程),由于执行任务需要一定时间,因此主线程至少在一段时间内不能提交任务,从而使得线程池有时间来处理完正在执行的任务。

10、Executors 提供了哪些创建线程池的方法?


答:

  • newFixedThreadPool:固定线程数的线程池。corePoolSize = maximumPoolSize,keepAliveTime为0,工作队列使用无界的LinkedBlockingQueue。适用于为了满足资源管理的需求,而需要限制当前线程数量的场景,适用于负载比较重的服务器。

  • newSingleThreadExecutor:只有一个线程的线程池。corePoolSize = maximumPoolSize = 1,keepAliveTime为0, 工作队列使用无界的LinkedBlockingQueue。适用于需要保证顺序的执行各个任务的场景。

  • newCachedThreadPool: 按需要创建新线程的线程池。核心线程数为0,最大线程数为 Integer.MAX_VALUE,keepAliveTime为60秒,工作队列使用同步移交 SynchronousQueue。该线程池可以无限扩展,当需求增加时,可以添加新的线程,而当需求降低时会自动回收空闲线程。适用于执行很多的短期异步任务,或者是负载较轻的服务器。

  • newScheduledThreadPool:创建一个以延迟或定时的方式来执行任务的线程池,工作队列为 DelayedWorkQueue。适用于需要多个后台线程执行周期任务。

  • newWorkStealingPool:JDK 1.8 新增,用于创建一个可以窃取的线程池,底层使用 ForkJoinPool 实现。