面试知识点

Posted 今夜月色很美

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试知识点相关的知识,希望对你有一定的参考价值。

1、cas

CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。

CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题。

  1. 循环时间长开销很大。
  2. 只能保证一个变量的原子操作。
  3. ABA问题。

循环时间长开销很大:

CAS 通常是配合无限循环一起使用的,我们可以看到 getAndAddInt 方法执行时,如果 CAS 失败,会一直进行尝试。如果 CAS 长时间一直不成功,可能会给 CPU 带来很大的开销。

只能保证一个变量的原子操作:

当对一个变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个变量操作时,CAS 目前无法直接保证操作的原子性。但是我们可以通过以下两种办法来解决:1)使用互斥锁来保证原子性;2)将多个变量封装成对象,通过 AtomicReference 来保证原子性。

什么是ABA问题?ABA问题怎么解决?

CAS 的使用流程通常如下:1)首先从地址 V 读取值 A;2)根据 A 计算目标值 B;3)通过 CAS 以原子的方式将地址 V 中的值从 A 修改为 B。

但是在第1步中读取的值是A,并且在第3步修改成功了,我们就能说它的值在第1步和第3步之间没有被其他线程改变过了吗?

如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。

2、手写模板模式

模板方法定义了一个算法的步骤,并允许子类为一个或者多个步骤提供具体实现

3、redis为什么设计成单线程的

基于内存而且使用多路复用技术,避免多线程上下文切换加锁等消耗。
引入多线程操作也是出于性能上的考虑,对于一些大键值对的删除操作,通过多线程减少对 Redis 主线程阻塞的时间。

4、union和union all

Union:对两个结果集进行并集操作,不包括重复行,同时进行默认规则的排序;

Union All:对两个结果集进行并集操作,包括重复行,不进行排序;

5、spring bean的生命周期

1、通过构造器或工厂方法创建 Bean 实例
2、注入bean的属性
3、如果bean实现了对应的aware接口,spring会设置BeanName、BeanFactory、ApplicationContext等
4、如果bean实现了BeanPostProcessor接口,BeanPostProcessor的postProcessBeforeInitialization()方法将被调用
5、调用 Bean 的初始化方法(注解@PostConstruct,xml方式init-method)
6、如果bean实现了BeanPostProcessor接口,BeanPostProcessor的postProcessAfterInitialization()接口方法将被调用
7、Bean可以使用了
8、当容器关闭时, 调用 Bean 的销毁方法

6、堆溢出和栈溢出

栈溢出场景:递归调用太深比如几万层递归

7、什么是aqs

Java中的大部分同步类(Lock、Semaphore、ReentrantLock等)都是基于AbstractQueuedSynchronizer(简称为AQS)实现的。AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。

Q:某个线程获取锁失败的后续流程是什么呢?

A:存在某种排队等候机制,线程继续等待,仍然保留获取锁的可能,获取锁流程仍在继续。

Q:既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?

A:是CLH变体的FIFO双端队列。(CLH:Craig、Landin and Hagersten队列,是单向链表)

Q:处于排队等候机制中的线程,什么时候可以有机会获取锁呢?

A:一个线程获取锁失败了,被放入等待队列,acquireQueued会把放入队列中的线程不断去获取锁,直到获取成功或者不再需要获取(中断)。详细看下2.3.1.3小节。

Q:如果处于排队等候机制中的线程一直无法获取锁,需要一直等待么?还是有别的策略来解决这一问题?

A:线程所在节点的状态会变成取消状态,取消状态的节点会从队列中释放,具体可见2.3.2小节。

Q:Lock函数通过Acquire方法进行加锁,但是具体是如何加锁的呢?

A:AQS的Acquire会调用tryAcquire方法,tryAcquire由各个自定义同步器实现,通过tryAcquire完成加锁过程。

参考https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html

8、mybatis为什么接口不需要实现类

使用了JDK动态代理的方式创建代理对象完成的,根据方法名找到EmployeeMapper.xml文件中对应id标识,执行sql语句完成查询操作。

9、分页的实现原理

PageInterceptor源码:

			List resultList;
            //调用方法判断是否需要进行分页,如果不需要,直接返回结果
            if (!dialect.skip(ms, parameter, rowBounds)) {
                //判断是否需要进行 count 查询
                if (dialect.beforeCount(ms, parameter, rowBounds)) {
                    //查询总数
                    Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                    //处理查询总数,返回 true 时继续分页查询,false 时直接返回
                    if (!dialect.afterCount(count, parameter, rowBounds)) {
                        //当查询总数为 0 时,直接返回空的结果
                        return dialect.afterPage(new ArrayList(), parameter, rowBounds);
                    }
                }
                resultList = ExecutorUtil.pageQuery(dialect, executor,
                        ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
            } else {
                //rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
                resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            }
            return dialect.afterPage(resultList, parameter, rowBounds);

10、redis缓存雪崩

参考另一篇博客redis学习笔记https://blog.csdn.net/noob9527/article/details/116451361

11、jdk自带的4种线程池

1 Executors.newCachedThreadPool()
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
2 Executors.newFixedThreadPool()
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
3 Executors.newScheduledThreadPool()
创建一个定长线程池,支持定时及周期性任务执行。
4 Executors.newSingleThreadExecutor()
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
Executors是jdk并发包下的一个工具类,几种线程池底层用的都是ThreadPoolExecutor
我们项目中用的是ThreadPoolTaskExecutor,是spring为我们提供的线程池类

12、springboot启动流程

创建一个StopWatch并执行start方法,这个类主要记录任务的执行时间配置
在文件META-INF\\spring.factories中获取SpringApplicationRunListener接口的实现类EventPublishingRunListener,主要发布SpringApplicationEvent
把输入参数转成DefaultApplicationArguments类
创建Environment并设置比如环境信息,系统熟悉,输入参数和profile信息
创建Application的上下文,根据WebApplicationTyp来创建Context类,如果非web项目则创AnnotationConfigApplicationContext,在构造方法中初始化AnnotatedBeanDefinitionReader和ClassPathBeanDefinitionScanner
在文件META-INF\\spring.factories中获取SpringBootExceptionReporter接口的实现类FailureAnalyzers
准备application的上下文
刷新上下文,在这里真正加载bean到容器中。如果是web容器,会在onRefresh方法中创建一个Server并启动。

13、工厂模式

public class SimpleFactory {

	public Pizza createPizza(String type) {
		
		Pizza pizza = null;
		if (type.equals("greek")) {
			return new GreekPizza();
		} else if (type.equals("cheese")) {
			return new CheessPizza();
		} else if (type.equals("pepper")) {
			return new PepperPizza();
		}
		return pizza;
	}
}

14、JDK1.8新特性

hashmap

HashMap中链长度大于8时采取红黑树的结构存储。(1.7的时候是链表结构)
红黑树,除了添加,效率高于链表结构。

ConcurrentHashMap

Jdk1.7时隔壁级别CocnurrentLevel(锁分段机制)默认为16。
JDK1.8采取了CAS算法
CAS原理主要涉及的有:锁机制、CAS 操作;

Optional类

Optional 类(java.util.Optional) 是一个容器类,代表一个值存在或不存在,原来用null 表示一个值不存在,现在Optional 可以更好的表达这个概念。并且可以避免空指针异常。
常用方法:

Optional.of(T t) : 创建一个Optional 实例
Optional.empty() : 创建一个空的Optional 实例
Optional.ofNullable(T t):若t 不为null,创建Optional 实例,否则创建空实例
isPresent() : 判断是否包含值
orElse(T t) : 如果调用对象包含值,返回该值,否则返回t
orElseGet(Supplier s) :如果调用对象包含值,返回该值,否则返回s 获取的值
map(Function f): 如果有值对其处理,并返回处理后的Optional,否则返回Optional.empty()
flatMap(Function mapper):与map 类似,要求返回值必须是Optional

15、BIO,NIO,AIO区别

BIO:Blocking I/O

NIO:Non-Blocking I/O

NIO使用了多路复用器机制,以socket使用来说,多路复用器通过不断轮询各个连接的状态,只有在socket有流可读或者可写时,应用程序才需要去处理它,在线程的使用上,就不需要一个连接就必须使用一个处理线程了,而是只是有效请求时(确实需要进行I/O处理时),才会使用一个线程去处理,这样就避免了BIO模型下大量线程处于阻塞等待状态的情景。

Selector 是NIO相对于BIO实现多路复用的基础,Selector 运行单线程处理多个 Channel,如果你的应用打开了多个通道,但每个连接的流量都很低,使用 Selector 就会很方便。例如在一个聊天服务器中。要使用 Selector , 得向 Selector 注册 Channel,然后调用它的 select() 方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新的连接进来、数据接收等。

AIO:Asynchronous I/O(异步非阻塞I/O)

NIO需要使用者线程不停的轮询IO对象,来确定是否有数据准备好可以读了,而AIO则是在数据准备好之后,才会通知数据使用者,这样使用者就不需要不停地轮询了。当然AIO的异步特性并不是Java实现的伪异步,而是使用了系统底层API的支持,在Unix系统下,采用了epoll IO模型,而windows便是使用了IOCP模型。

16、synchronized实现原理

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.

3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

执行monitorexit的线程必须是objectref所对应的monitor的所有者。

指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

锁的4中状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态

偏向锁

当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

轻量级锁

轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。

重量级锁

自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

注意:为了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了;偏向锁升级为轻量级锁也不能再降级为偏向锁。一句话就是锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态。

17、@Autowire和@Resource区别

1.@Autowire是Spring开发的,而@Resource是jdk开发的。

2.@Autowire是按照type来装配,而@Resource是按照name,如果name找不到,那么按照type。

18、聚簇索引和非聚簇索引区别

聚簇索引的叶子节点就是数据节点,而非聚簇索引的叶子节点仍然是索引节点,只不过有指向对应数据块的指针。

InnoDB的的二级索引的叶子节点存放的是KEY字段加主键值。因此,通过二级索引查询首先查到是主键值,然后InnoDB再根据查到的主键值通过主键索引找到相应的数据块。而MyISAM的二级索引叶子节点存放的还是列值与行号的组合,叶子节点中保存的是数据的物理地址。所以可以看出MYISAM的主键索引和二级索引没有任何区别,主键索引仅仅只是一个叫做PRIMARY的唯一、非空的索引,且MYISAM引擎中可以不设主键。

19、ribbon常见四大负载均衡算法

RoundRobinRule 轮询策略
RoundRobinRule 随机策略
WeightedResponseTimeRule响应时间加权重策略
RetryRule 重试策略

20、rabbitmq和kafka的不同点

rabbitmq适合单点,集群稳定性不好
rabbit消息可靠不丢失,流量级别比kafka小
kafka适合集群,消息可能会丢失(消费者可以程序控制offset来保证消息不丢失)

21、工厂模式应用场景

消费者不关心它所要创建对象的类(产品类)的时候。
消费者知道它所要创建对象的类(产品类),但不关心如何创建的时候。
例如:hibernate里通过sessionFactory创建session、通过代理方式生成ws客户端时,通过工厂构建报文中格式化数据的对象。

22、虚拟化和容器化之间的区别

虚拟化是一种可以模拟您的物理硬件(例如CPU核心,内存,磁盘)并将其表示为独立计算机的技术。它具有自己的Guest OS,内核,进程,驱动程序等。因此,它是硬件级虚拟化。最常用的技术是VMware和VirtualBox
容器化是操作系统级别的虚拟化。它不会模拟整个物理机器。它只是模拟计算机的操作系统。因此,多个应用程序可以共享同一OS内核。容器扮演着与虚拟机相似的角色,但是没有硬件虚拟化。最常见的容器技术是Docker

23、线程池饱和策略

饱和策略分为:Abort 策略, CallerRuns 策略,Discard策略,DiscardOlds策略。

Abort策略:默认策略,新任务提交时直接抛出未检查的异常RejectedExecutionException,该异常可由调用者捕获。
CallerRuns策略:为调节机制,既不抛弃任务也不抛出异常,而是将某些任务回退到调用者。不会在线程池的线程中执行新的任务,而是在调用exector的线程中运行新的任务。
Discard策略:新提交的任务被抛弃。
DiscardOldestPolicy抛弃旧任务策略(新任务会被加入队列,队列中的旧任务会被抛弃)。

24、springboot条件装配有哪些?如何自定义条件装配

springboot继承@Conditional的条件装配注解有很多,如截图所示:

通过继承@Conditional注解的方式可以自定义条件装配

25、jvm垃圾回收算法

1、标记–清除算法

执行步骤:

  • 标记:遍历内存区域,对需要回收的对象打上标记。
  • 清除:再次遍历内存,对已经标记过的内存进行回收。

缺点:

  • 效率问题;遍历了两次内存空间(第一次标记,第二次清除)。
  • 空间问题:容易产生大量内存碎片,当再需要一块比较大的内存时,无法找到一块满足要求的,因而不得不再次出发GC。

2、复制算法

将内存划分为等大的两块,每次只使用其中的一块。当一块用完了,触发GC时,将该块中存活的对象复制到另一块区域,然后一次性清理掉这块没有用的内存。下次触发GC时将那块中存活的的又复制到这块,然后抹掉那块,循环往复。

优点

  • 相对于标记–清理算法解决了内存的碎片化问题。
  • 效率更高(清理内存时,记住首尾地址,一次性抹掉)。

缺点:

  • 内存利用率不高,每次只能使用一半内存。

改进

研究表明,新生代中的对象大都是“朝生夕死”的,即生命周期非常短而且对象活得越久则越难被回收。在发生GC时,需要回收的对象特别多,存活的特别少,因此需要搬移到另一块内存的对象非常少,所以不需要1:1划分内存空间。而是将整个新生代按照8 : 1 : 1的比例划分为三块,最大的称为Eden(伊甸园)区,较小的两块分别称为To Survivor和From Survivor。

首次GC时,只需要将Eden存活的对象复制到To。然后将Eden区整体回收。再次GC时,将Eden和To存活的复制到From,循环往复这个过程。这样每次新生代中可用的内存就占整个新生代的90%,大大提高了内存利用率。

但不能保证每次存活的对象就永远少于新生代整体的10%,此时复制过去是存不下的,因此这里会用到另一块内存,称为老年代,进行分配担保,将对象存储到老年代。若还不够,就会抛出OOM。

老年代:存放新生代中经过多次回收仍然存活的对象(默认15次)。

3、标记–整理算法

因为前面的复制算法当对象的存活率比较高时,这样一直复制过来,复制过去,没啥意义,且浪费时间。所以针对老年代提出了“标记整理”算法。

执行步骤:

  • 标记:对需要回收的进行标记
  • 整理:让存活的对象,向内存的一端移动,然后直接清理掉没有用的内存。

4、分代收集算法

当前大多商用虚拟机都采用这种分代收集算法,这个算法并没有新的内容,只是根据对象的存活的时间的长短,将内存分为了新生代和老年代,这样就可以针对不同的区域,采取对应的算法。如:

  • 新生代,每次都有大量对象死亡,有老年代作为内存担保,采取复制算法。
  • 老年代,对象存活时间长,采用标记整理,或者标记清理算法都可。

27、redis内存淘汰策略

volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。

allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)。

volatile-lfu:当内存不足以容纳新写入数据时,在过期密集的键中,使用LFU算法进行删除key。

allkeys-lfu:当内存不足以容纳新写入数据时,使用LFU算法移除所有的key。

volatile-random:当内存不足以容纳新写入数据时,在设置了过期的键中,随机删除一个key。

allkeys-random:当内存不足以容纳新写入数据时,随机删除一个或者多个key。

volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。

28、CPU密集型和IO密集型

IO密集型:大量网络,文件操作
CPU 密集型:大量计算,cpu 占用越接近 100%, 耗费多个核或多台机器

线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。

是否使用线程池就一定比使用单线程高效呢?

答案是否定的,比如Redis就是单线程的,但它却非常高效,基本操作都能达到十万量级/s。从线程这个角度来看,部分原因在于:

  • 多线程带来线程上下文切换开销,单线程就没有这种开销

29、jvm内存模型

堆区(heap ):

存储的全部是对象,每个对象都包含一个与之对应的class的信息。
jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身

栈区(stack):

基本类型变量区、执行环境上下文、操作指令区
每一个线程包含一个stack区,只保存基本数据类型的对象和自定义对象的引用(不是对象),对象都存放在共享堆中;
每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。

方法区(meathod area):

又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。
方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。
运行时常量池都分配在 Java 虚拟机的方法区之中

本地方法栈(Native Method Stack)

当JVM在执行Native方法时,会在此区域中创建一个栈帧来存放方法的各种信息,比如返回值,局部变量表和各种对象引用等,方法开始执行前就先创建栈帧入栈,执行完后就出栈。

程序计数器(Program Counter Register)

占用很小的一片区域,我们知道JVM执行代码是一行一行执行字节码,所以需要一个计数器来记录当前执行的行数。

堆里面分为年轻代老年代

年轻代默认占1/3(Eden默认8/10 , from默认1/10 , to默认 1/10)
老年代默认占2/3

minor gc :对年轻代的垃圾回收称作初级回收 , 轻GC

Full GC 是发生在老年代的垃圾收集动作,所采用的是标记-清除算法

以上是关于面试知识点的主要内容,如果未能解决你的问题,请参考以下文章

android小知识点代码片段

前端面试题之手写promise

2021-12-24:划分字母区间。 字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。 力扣763。某大厂面试

Java进阶之光!2021必看-Java高级面试题总结

线程学习知识点总结

Java工程师面试题,二级java刷题软件