• 常见面试题整理(2022-11)



    一、Java

    1、锁机制

    Java Hotspot 虚拟机中,每个对象都有对象头(包括 class 指针和 Mark Word)。Mark Word 平时存储这个对象的 哈希码、分代年龄,当加锁时,这些信息就根据情况被替换为 标记位(轻重量级锁)、线程锁记录指针、重量级锁指针、线程ID等内容。

    Mark Word:8位。加锁替换后,把之前的暂存到栈针中。

    使用场景锁类型
    多个线程交替执行轻量级锁
    编号锁类型
    00轻量锁
    01无锁
    10重量锁

    在这里插入图片描述

    1 轻量级锁

    交替级使用

    2、锁膨胀

    交替级使用

    3、线程状态

    线程阻塞:

    线程阻塞,保存当前状态,需要经过唤醒才能继续使用。
    
    • 1

    线程自旋重试:

    智能化自旋尝试,自旋时间不固定,会基于上次成功的时间进行判断。线程不会停止,多核CPU进行获取锁机制。
    
    • 1

    2、HashMap并发下产生问题

    • 会产生死循环的问题
    • put时数据丢失问题
    • 容量size的不准确
    • 重Hash问题

    会产生死循环的问题
    在1.8中,引入了红黑树优化数组链表,同时改成了尾插,按理来说是不会有环了,但是还是会出现死循环的问题,在链表转换成红黑数的时候无法跳出等多个地方都会出现这个问题。

    put数据丢失描述
    在下方代码注释处,线程已经拿到了头结点和hash桶,若此时cpu挂起,重新进入执行前,这个hash桶已经被其他线程更改过,那么在该线程重入后,他将持有一个过期的桶和头结点,并且覆盖之前其他线程的记录,造成了数据丢失。

    容量size的不准确
    size只是用了transient(不参与序列化)关键字修饰,在各个线程中的size不会及时同步,在多个线程操作的时候,size将会被覆盖。

    3、线程池创建方式

    线程池创建方式
    在这里插入图片描述

    //  可缓存线程池
    ExecutorService executorService1 = Executors.newCachedThreadPool();
    //  单线程线程池
    ExecutorService executorService2 = Executors.newSingleThreadExecutor();
    //  固定数量线程池
    ExecutorService executorService3 = Executors.newFixedThreadPool(1);
    //  并行线程池
    ExecutorService executorService4 = Executors.newWorkStealingPool(3);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    //  单线程线程池
    ScheduledExecutorService scheduledExecutor1 = Executors.newSingleThreadScheduledExecutor();
    //  延迟线程池
    ScheduledExecutorService scheduledExecutor2 = Executors.newScheduledThreadPool(1);
    
    • 1
    • 2
    • 3
    • 4

    阿里巴巴不推荐的4种线程池创建方式
    线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

    //  可缓存线程池
    ExecutorService executorService1 = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
    //  单线程线程池
    ExecutorService executorService2 = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
    //  固定数量线程池
    ExecutorService executorService3 = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
    //  并行线程池
    ExecutorService executorService4 = new ForkJoinPool(3, ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    4、Spring循环依赖

    二、MySQL相关

    1、mysql的redo、undo应用场景和区别

    与不同引擎的关系核心作用生命周期日志类型
    undo log属于innodb引擎独有回滚,保证事务的“原子性”,事务日志事务开始前,以类似“快照”的方式记录现场逻辑日志逻辑日志:记录语句原始逻辑(SQL)。
    redo log属于innodb引擎独有重做,保证事务的“持久性”,事务日志事务开始后记录,prepare阶段落盘物理日志物理日志:记录在某个页面上做了什么修改
    binlog工作在mysql的Server层,与使用哪种引擎无关实现主从节点数据的复制事务执行期间记录,commit阶段完成前落盘逻辑日志

    2、MVCC

    Multi-Version Concurrency Control 多版本并发控制

    3、MySQL的回表

    即基于非主键索引的查询需要多扫描一棵索引树。

    回表,顾名思义就是回到表中,也就是先通过普通索引扫描出数据所在的行,再通过行主键ID 取出索引中未包含的数据。所以回表的产生也是需要一定条件的,如果一次索引查询就能获得所有的select 记录就不需要回表,如果select 所需获得列中有其他的非索引列,就会发生回表动作。即基于非主键索引的查询需要多扫描一棵索引树。

    在这里插入图片描述

    三、Redis

    1、List数据量过大会有什么现象,以及解决方式?

    数据量过大:

    会查询时间太长,内存消耗过大。

    解决办法:

    • 采用主从、集群模式部署,将读写分离。降低但服务器节点的CPU和带宽消耗。
    • 采用分片处理:

    2、大Key和大value危害及处理

    • 内存不均:单value较大时,可能会导致节点之间的内存使用不均匀,间接地影响key的部分和负载不均匀;
    • 阻塞请求:redis为单线程,单value较大读写需要较长的处理时间,会阻塞后续的请求处理;
    • 阻塞网络:单value较大时会占用服务器网卡较多带宽,可能会影响该服务器上的其他Redis实例或者应用。

    大key的风险:

    • 读写大key会导致超时严重,甚至阻塞服务。

    • 如果删除大key,DEL命令可能阻塞Redis进程数十秒,使得其他请求阻塞,对应用程序和Redis集群可用性造成严重的影响。

    redis使用会出现大key的场景:

    • 单个简单key的存储的value过大;

    • hash、set、zset、list中存储过多的元素。

    解决问题:

    1.单个简单key的存储的value过大的解决方案:

    将大key拆分成对个key-value,使用multiGet方法获得值,这样的拆分主要是为了减少单台操作的压力,而是将压力平摊到集群各个实例中,降低单台机器的IO操作。

    2.hash、set、zset、list中存储过多的元素的解决方案:

    1).类似于第一种场景,使用第一种方案拆分;

    2).以hash为例,将原先的hget、hset方法改成(加入固定一个hash桶的数量为10000),先计算field的hash值模取10000,确定该field在哪一个key上。

    将大key进行分割,为了均匀分割,可以对field进行hash并通过质数N取余,将余数加到key上面,我们取质数N为997。

    那么新的key则可以设置为:

    newKey = order_20200102_String.valueOf( Math.abs(order_id.hashcode() % 997) )

    field = order_id

    value = 10

    hset (newKey, field, value) ;

    hget(newKey, field)

    3、跳跃表

    跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。

    加来一层索引后,查找一个节点需要遍历的节点个数减少了,也就是说查询效率得到了提升,同理我们在一级索引的基础上,在加二级索引。
    实现结构:
    Redis的跳跃表由zskiplistNode和zskiplist两个结构定义。

    • zskiplistNode:其中zskiplistNode结构用于表示跳跃表节点。
    • zskiplist: 结构则用于保存跳跃表节点的相关信息。

    zskiplist结构:

    • header:指向跳跃表的表头节点,通过这个指针程序定位表头节点的时间复杂度就为O(1)。
    • tail:指向跳跃表的表尾节点,通过这个指针程序定位表尾节点的时间复杂度就为O(1)。
    • level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内);通过这个属性可以再O(1)的时间复杂度内获取层高最高的节点的层数。
    • length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内)通过这个属性,程序可以再O(1)的时间复杂度内返回跳跃表的长度。


    参考地址

    https://blog.csdn.net/sssxlxwbwz/article/details/123769262
    
    • 1

    4、Redisson原理

    1. 设置Key

    2. 看门口机制

    四、Springboot相关

    1、启动类注解

    //	注册到容器(@Configuration)
    @SpringbootConfiguration
    //  自动配置
    @EnableAutoConfiguration
    //  包扫描
    @ComponentScan
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • @SpringbootConfiguration:功能类似spring的@Configuration,扫描路径下的Bean会自动被装载到spring上下文中,代替以前用xml配置的Bean。
    • @EnableAutoConfiguration:自动映射扫描路径下的Bean,通常根据类的路径和Bean的上下文,自动进行配置。
    • @ComponentScan:会自动扫描路径下的所有@Component标注的类,包括@Service、@Repository、@Controller

    五、Spring Cloud相关

    1、euraka和zookeeper区别

    2、CAP理论

    • Consistency(一致性)
    • Availability(可用性)
    • Partition tolerance (分区容错性)

    **Consistency(一致性):**用户访问分布式系统中的任意节点,得到的数据必须一致。

    Availability (可用性):用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝。

    Partition(分区):因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区。

    Tolerance(容错):在集群出现分区时,整个系统也要持续对外提供服务。

    案例:

    ES集群:是低可用性,高一致性。属于CP

    Nacos 集群:默认是AP模式。即虽然有主从节点之分,但是在恢复模式 选举过程中仍可对外提供服务,不影响新服务注册。因节点宕机而丢失的注册信息会在节点重启后恢复。

    七、Kafka

    1、消息不丢失

    生产端

    • acks 参数设置为 all,强制要求写入所有 ISR 中的副本成功后才认为是成功。
    • retries 参数设置为 Integer.MAX_VALUE,在出现一条消息发送失败之后,就一直重试直到成功为止。

    消费端

    • 关闭自动提交,即设置 enable.auto.commit 为 false,同时使用同步提交及在代码中使用commitOffsetsSync 函数按照 offset 的维度进行消息提交。

    服务端

    • 设置log.flush.interval.messages参数为 1,也就是每写入一条消息就强制刷盘。默认情况下 kafka 是不控制刷盘的,交给 OS 去控制。
    • 设置 Topic 的副本数至少大于等于 2,通常情况下是默认为 3。
    • 对Kafka进行限速。

    2、重复消费

    • 基于数据库的唯一键:我们可以通过消息的内容拼成一个唯一的 key。然后创建一个幂等表,其中可以就两列 ,其中设置 key列为唯一键。每次进行消息的业务处理前,进行幂等判断,也就是朝表中插入一个key,如果报了对应的违反唯一性的异常,那么就跳过该消息的处理。
    • 基于缓存:实现原理跟用 DB 基本一致,不过可以修改为判断 key 是否存在于缓存中,如果存在则跳过否则存入后再进行业务处理。

    3、顺序消费

    • 1个Topic(主题)只创建1个Partition(分区),这样生产者的所有数据都发送到了一个Partition(分区),保证了消息的消费顺序。
    • 发送消息保证同步发送,不能异步发送。

    4、Kafka高性能的原因

    参考地址

    https://blog.csdn.net/qq_38571987/article/details/118492557
    
    • 1
    • 顺序写入磁盘,增加IO性能
    • 页缓存pageCache
    • 零拷贝
    • 网络数据采用压缩算法

    1、顺序写入磁盘,增加IO性能
    采用顺序写入磁盘的方式:顺序写入磁盘的速度是要快于随机写入内存的。
    Kafka就是采用了顺序写入的方式,每次新的内容写入都是采用文件追加的方式,这也就以为着每次新写入的数据都是在文件的结尾,并且对于之前已经写入的内容是不能够进行修改的。
    2、页缓存pageCache
    Kafka会将一些热点数据放在PageCache中。(热点数据:最近访问的数据)

    • 当查询数据时,先从PageCache中进行查找,如果PageCache没有,再去磁盘中查找,并将磁盘中的数据拷贝到PageCache中。这样就可以避免每次数据查询都直接去磁盘查询。
    • 预查询:因为磁盘查询性能低,如果一次没有查到还会进行第二次,所以在第一次查询的时候,PageCache会进行预查询的操作,比如需要查询0-32k的数据,PageCache会将32-64k的数据也加载进来,增加查询的命中率。

    3、零拷贝
    因为Kafka的消息是存在磁盘的,消息是在生产者,Broker,消费者中进行网络传输的,这里就涉及到了消息在磁盘到网络的转换。而零拷贝的作用就是通过减少用户态和内核态的转换,从而减少消息在磁盘到网络传输的资源损耗。

    4、网络数据采用压缩算法
    在Kafka中消息是在生产者,Broker,消费者进行传输的,Kafka采用的数据压缩的方式,以时间换空间,通过cpu时间的增加来尽量的减少磁盘空间的占用和网络IO的传输量,Kafka中消息的压缩是发生在生产者和Broker端的。

    八、设计模式

    1、分类

    SpringIOC:

    工程模式、单例模式、装饰者模式

    Spring AOP:

    代理模式、观察者模式

    Spring MVC:

    委派模式、适配器模式

    Spring JDBC:

    模板方法模式

    2、单例模式

    懒汉式

    public class Signletion {
        private static Signletion signletion = null;
    
        private Signletion() {
        }
    
        public static Signletion getSignletion() {
            if (signletion == null) {
                synchronized (Signletion.class) {
                    if (signletion == null) {
                        signletion = new Signletion();
                    }
                }
            }
            return signletion;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    饿汉式

    public class Signletion {
        private static final Signletion signletion = new Signletion();
    
        private Signletion() {
        }
    
        public static Signletion getSignletion() {
            return signletion;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    3、AOP底层实现

    创建步骤

    1. 首先、需要创建代理工厂,代理工厂需要3个重要信息:截器数组,目标对象接口数组、目标对象。
    2. 创建代理工厂时,默认会在拦截器数组尾部再增加一个默认拦截器(用于最终的调用目标方法)。
    3. 当调用getProxy方式的时候,会根据接口数量大于0条件返回一个代理对象(JDK for Cglib)。

    代理的调用

    1. 当对代理对象进行调用时,就会触发外层拦截器。
    2. 外层拦截器根据代理对象配置信息,创建内层拦截器链。创建的过程中,会根据表达式判断当前拦截是否匹配这个拦截器(职责链模式)。
    3. 当整个链条执行到最后时,就会触发创建代理时就会触发尾部默认拦截器,从而调用目标方法,最后返回。

    九、ES

    1、ES的QPS多少?

    2、分片机制

    Elasticsearch 是一个高度可用的分布式搜索引擎。每个索引都分解为分片(shard),每个分片可以有一个或多个副本。默认情况下,创建一个索引,每个分片有1个分片和1个副本(1/1)。可以使用许多拓扑,包括1/10(提高搜索性能,多个副本可以帮我们提高搜索的速度)或20/1(提高索引性能,多个主分片可以帮我们提高导入数据的速度)。

    为了使用 Elasticsearch 的分布式特性,只需启动更多节点并关闭节点。系统将继续为索引的最新数据提供请求(确保使用正确的 HTTP 端口)。

    d'd

    十、多线程

    1、volatile关键字

    • 每个线程操作数据的时候会把数据从主内存读取到⾃⼰的⼯作内存,如果他操作了数据并且写会了,他其他已经读取的线程的变量副本就会失效了,需要都数据进⾏操作⼜要再次去主内存中读取了。

    • volatile保证不同线程对共享变量操作的可⻅性,也就是说⼀个线程修改了volatile修饰的变量,当修改写回主内存时,另外⼀个线程⽴即看到最新的值。

    缺点:

    由于Volatile的MESI缓存⼀致性协议,需要不断的从主内存嗅探和cas不断循环,⽆效交互会导致总线带宽达到峰值。

    MESI(缓存⼀致性协议)

    当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存⾏置为⽆效状态,因此当其他CPU需要读取这个变量时,发现⾃⼰缓存中缓存该变量的缓存⾏是⽆效的,那么它就会从内存重新读取。

    总结:

    1. volatile修饰符适⽤于以下场景:某个属性被多个线程共享,其中有⼀个线程修改了此属性,其他线
      程可以⽴即得到修改后的值,⽐如booleanflag;或者作为触发器,实现轻量级同步。
    2. volatile属性的读写操作都是⽆锁的,它不能替代synchronized,因为它没有提供原⼦性和互斥性。
      因为⽆锁,不需要花费时间在获取锁和释放锁_上,所以说它是低成本的。
    3. volatile只能作⽤于属性,我们⽤volatile修饰属性,这样compilers就不会对这个属性做指令重排
      序。
    4. volatile提供了可⻅性,任何⼀个线程对其的修改将⽴⻢对其他线程可⻅,volatile属性不会被线程缓
      存,始终从主 存中读取。
    5. volatile提供了happens-before保证,对volatile变量v的写⼊happens-before所有其他线程后续对v的
      读操作。
    6. volatile可以使得long和double的赋值是原⼦的。
    7. volatile可以在单例双重检查中实现可⻅性和禁⽌指令重排序,从⽽保证安全性。

    2、volatile与synchronized的区别

    • volatile只能修饰实例变量和类变量,⽽synchronized可以修饰⽅法,以及代码块。

    • volatile保证数据的可⻅性,但是不保证原⼦性(多线程进⾏写操作,不保证线程安全);⽽synchronized是⼀种排他(互斥)的机制。 volatile⽤于禁⽌指令重排序:可以解决单例双重检查对象初始化代码执⾏乱序问题。

    • volatile可以看做是轻量版的synchronized,volatile不保证原⼦性,但是如果是对⼀个共享变量进⾏多个线程的赋值,⽽没有其他的操作,那么就可以⽤volatile来代替synchronized,因为赋值本身是有原⼦性的,⽽volatile⼜保证了可⻅性,所以就可以保证线程安全了。

    十一、大数据相关

    1、rowkey设计原则

    • 唯一性:RowKey必须能够唯一的识别一行数据;无论应用时什么样的负载特点,RowKey字段都应该参考最高频的查询场景。数据库通常都是以如何高效的读取和消费数据为目的,而不是存储本身。而后,结合具体的负载特点,再对选取的RowKey字段值进行改造,组合字段场景下需要重点考虑字段的顺序。
    • RowKey 长度原则:RowKey 是一个二进制码流,可以是任意字符串,最大长度 64kb ,实际应用中一般为 10-100bytes,以 byte[] 形式保存,一般设计成定长。建议越短越好,不要超过 16 个字节。列族、列名的命名在保证可读的情况下也应尽量短。value 永远和它的 key 一起传输的。
    • 排序原则:RowKey 是按照字典顺序排序存储的,因此,设计 RowKey 的时候,要充分利用这个排序的特点,将经常读取的数据存储到一块,将最近可能会被访问的数据放到一块。一个常见的数据处理问题是快速获取数据的最近版本,使用反转的时间戳作为 RowKey 的一部分对这个问题十分有用,可以用 Long.Max_Value-timestamp追加到key的末尾。例如 [key][reverse_timestamp] , [key]的最新值可以通过scan [key]获得[key]的第一条记录,因为 HBase 中 RowKey 是有序的,第一条记录是最后录入的数据。

    00000、其它

    1、CAS

    CAS(compare and swap):CPU一条并发原句。比较并交换。

    判断内存中,某个位置是否为预期值。

    存在于sun.misc.Unsafe中方法。

    缺点

    • 循环时间长,开销大。

    • 只能保证1个共享变量的原子操作。

      • 会有ABA问题。
    • spring boot中的start装载上去的。

    • 实现MyBatis的自动。

    • 谁去解析xml的。

    • 组装之前的操作,标签的解析。

    • 进程线程区别。

    • 异常回滚。

  • 相关阅读:
    GerbView生产高级软件,支持新旧表单
    Selenium爬取内容并存储至MySQL数据库
    map映射数组与引用类型处理技巧
    Mysql数据库架构介绍
    基于SSM的飞机航班管理系统
    React技巧之表单提交获取input值
    第一章 基础算法
    Java基础进阶多线程-生产者和消费者模式
    基本算法-冒泡排序
    你知道吗?chrome自动更新到104版本,居然引起Java服务内存泄漏
  • 原文地址:https://blog.csdn.net/weixin_44624117/article/details/127999031