2023秋招面试大厂高频面试题总结,必备八股文(Java基础,Redis,集合),自测day1

hashMap底层,redis三种常见问题及解决等感觉常问,还有MySQL知识点常问,之后总结

(2023秋招提前批百度面试)

1.Java中HashMap的实现原理、扩容机制、转为红黑树的规则、为什么选择红黑树

2.为什hashmap不是线程安全的

(2023秋招提前批字节面试)

3.问redis项目和具体实现

4.redis为什么快

5.缓存击穿,逻辑过期,缓存空值

6.redis分布式锁怎么做

7.redis数据一致性

(2023秋招提前批快手面试)

8.Hashmap底层

9.HashMap的扩容机制,  线程安全。

集合类

一,Java中有哪些容器(集合类)?

Java中的集合类主要由Collection和Map这两个接口派生而出,其中Collection接口又派生出三个子接口,分别是Set、List、Queue。所有的Java集合类,都是Set、List、Queue、Map这四个接口的实现类,这四个接口将集合分成了四大类,其中

Set代表无序的,元素不可重复的集合;

List代表有序的,元素可以重复的集合;

Queue代表先进先出(FIFO)的队列;

Map代表具有映射关系(key-value)的集合。

这些接口拥有众多的实现类,其中最常用的实现类有HashSet、TreeSet、ArrayList、LinkedList、ArrayDeque、HashMap、TreeMap等。


二,Java中的容器,线程安全和线程不安全的分别有哪些?

java.util包下的集合类大部分都是线程不安全的,例如我们常用的HashSet、TreeSet、ArrayList、LinkedList、ArrayDeque、HashMap、TreeMap,这些都是线程不安全的集合类,但是它们的优点是性能好。如果需要使用线程安全的集合类,则可以使用Collections工具类提供的synchronizedXxx()方法,将这些集合类包装成线程安全的集合类。

java.util包下也有线程安全的集合类,例如Vector、Hashtable。这些集合类都是比较古老的API,虽然实现了线程安全,但是性能很差。所以即便是需要使用线程安全的集合类,也建议将线程不安全的集合类包装成线程安全集合类的方式,而不是直接使用这些古老的API。

从Java5开始,Java在java.util.concurrent包下提供了大量支持高效并发访问的集合类,它们既能包装良好的访问性能,有能包装线程安全。这些集合类可以分为两部分,它们的特征如下:

以Concurrent开头的集合类:

以Concurrent开头的集合类代表了支持并发访问的集合,它们可以支持多个线程并发写入访问,这些写入线程的所有操作都是线程安全的,但读取操作不必锁定。以Concurrent开头的集合类采用了更复杂的算法来保证永远不会锁住整个集合,因此在并发写入时有较好的性能。

以CopyOnWrite开头的集合类:

以CopyOnWrite开头的集合类采用复制底层数组的方式来实现写操作。当线程对此类集合执行读取操作时,线程将会直接读取集合本身,无须加锁与阻塞。当线程对此类集合执行写入操作时,集合会在底层复制一份新的数组,接下来对新的数组执行写入操作。由于对集合的写入操作都是对数组的副本执行操作,因此它是线程安全的。


三,介绍一下HashMap底层的实现原理


它基于hash算法,通过put方法和get方法存储和获取对象。

存储对象时,我们将K/V传给put方法时,它调用K的hashcode计算hash从而得到bucket,进一步存储,HashMap会根据当前的bucket的占用情况自动调整容量大小。获取对象时,我们将K的值传给get方法,它调用hashcode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。

如果发生碰撞时,HashMap通过链表将产生碰撞冲突元素组织起来。在java 8 中,如果bucket中冲突的元素超过某个限制(默认8个),则用红黑树来替换链表,从而提高速度。

四,介绍一下HashMap的扩容机制


1.数组的初始容量为16,二容量时以2的次方进行扩充的,一是为了提高性能使用足够大的数组,二是为了使用位运算替代模预算(性能提高)。
2.数组是否需要扩充是通过负载因子判断的,如果当前元素个数为数组容量的0.75时,就会扩充数组,负载因子就是0.75,可有构造器传入,我们也可以设置大于1的负载因子,这样就永远不会扩冲,牺牲性能,节省内存。
3.为了解决碰撞,数组中的元素时单项链表存储的,当链表长度达到一个阈值(7或8),会将链表转化为红黑树提高性能。二链表长度缩小到一个阈值时(6),又会将红黑树转化为链表提高性能。
4.检查链表长度转换成红黑树之前,会判断数组是否达到某个阈值(64),如果没有达到这个容量,就会放弃转化,先去扩充数组。


 

五,HashMap中的循环链表是如何产生的?


在多线程的情况下,当重新调整HashMap大小的时候,就会存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历。如果条件竞争发生了,那么就会产生死循环了。

六,HashMap为什么用红黑树而不用B树?


B/B+树多用于外存上时,B/B+树也被称为一个磁盘友好的数据结构。

HashMap是以数组+链表的形式,链表由于查找慢的特点,所以需要被查找效率更高的树结构来替换。如果用B/B+树的话,在数量不多的情况下,数据就会“挤在”一个节点里面,遍历效率就退化成了链表。

七,HashMap为什么线程不安全?


HashMap在并发执行put方法时,会形成循环链表,造成死循环。


八,HashMap如何实现线程安全?


1.直接使用Hashtable类;
2.直接使用ConcurrentHashMap;
3.使用Collections将HashMap包装成线程安全的Map;


九,HashMap是如何解决哈希冲突的?


为了解决碰撞,数组中的元素是单向链表类型。当链表长度到达一个阈值时,会将链表转换成红黑树提高性能。而当链表长度缩小到另一个阈值时,又会将红黑树转换回单向链表提高性能。

 

Java基础类


一,异常处理


在Java中,可以按照如下三个步骤处理异常:

捕获异常

将业务代码包裹在try块内部,当业务代码中发生任何异常时,系统都会为此异常创建一个异常对象。创建异常对象之后,JVM会在try块之后寻找可以处理它的catch块,并将异常对象交给这个catch块处理。

处理异常

在catch块中处理异常时,应该先记录日志,便于以后追溯这个异常。然后根据异常的类型、结合当前的业务情况,进行相应的处理。比如,给变量赋予一个默认值、直接返回空值、向外抛出一个新的业务异常交给调用者处理,等等。

回收资源

如果业务代码打开了某个资源,比如数据库连接、网络连接、磁盘文件等,则需要在这段业务代码执行完毕后关闭这项资源。并且,无论是否发生异常,都要尝试关闭这项资源。将关闭资源的代码写在finally块内,可以满足这种需求,即无论是否发生异常,finally块内的代码总会被执行。


二,finally是无条件执行的吗?


不管try块中的代码是否出现异常,也不管哪一个catch块被执行,甚至在try块或catch块中执行了return语句,finally块总会被执行。

三,说一说你对static关键字的理解

在Java类里只能包含成员变量、方法、构造器、初始化块、内部类(包括接口、枚举)5种成员,而static可以修饰成员变量、方法、初始化块、内部类(包括接口、枚举),以static修饰的成员就是类成员。类成员属于整个类,而不属于单个对象。

对static关键字而言,有一条非常重要的规则:类成员(包括成员变量、方法、初始化块、内部类和内部枚举)不能访问实例成员(包括成员变量、方法、初始化块、内部类和内部枚举)。因为类成员是属于类的,类成员的作用域比实例成员的作用域更大,完全可能出现类成员已经初始化完成,但实例成员还不曾初始化的情况,如果允许类成员访问实例成员将会引起大量错误。

四,说一说hashCode()和equals()的关系


hashCode()用于获取哈希码(散列码),eauqls()用于比较两个对象是否相等,它们应遵守如下规定:

如果两个对象相等,则它们必须有相同的哈希码。

如果两个对象有相同的哈希码,则它们未必相等。

五,为什么要重写hashCode()和equals()?


Object类提供的equals()方法默认是用==来进行比较的,也就是说只有两个对象是同一个对象时,才能返回相等的结果。而实际的业务中,我们通常的需求是,若两个不同的对象它们的内容是相同的,就认为它们相等。鉴于这种情况,Object类中equals()方法的默认实现是没有实用价值的,所以通常都要重写。

六,==和equals()有什么区别?


==运算符:

作用于基本数据类型时,是比较两个数值是否相等;

作用于引用数据类型时,是比较两个对象的内存地址是否相同,即判断它们是否为同一个对象;

equals()方法:

没有重写时,Object默认以 == 来实现,即比较两个对象的内存地址是否相同;

进行重写后,一般会按照对象的内容来进行比较,若两个对象内容相同则认为对象相等,否则认为对象不等。

由于hashCode()与equals()具有联动关系(参考“说一说hashCode()和equals()的关系”一题),所以equals()方法重写时,通常也要将hashCode()进行重写,使得这两个方法始终满足相关的约定。

七,说一说String和StringBuffer有什么区别


String类是不可变类,即一旦一个String对象被创建以后,包含在这个对象中的字符序列是不可改变的,直至这个对象被销毁。

StringBuffer对象则代表一个字符序列可变的字符串,当一个StringBuffer被创建以后,通过StringBuffer提供的append()、insert()、reverse()、setCharAt()、setLength()等方法可以改变这个字符串对象的字符序列。一旦通过StringBuffer生成了最终想要的字符串,就可以调用它的toString()方法将其转换为一个String对象。

八, 说一说StringBuffer和StringBuilder有什么区别


StringBuffer、StringBuilder都代表可变的字符串对象,它们有共同的父类 AbstractStringBuilder,并且两个类的构造方法和成员方法也基本相同。不同的是,StringBuffer是线程安全的,而StringBuilder是非线程安全的,所以StringBuilder性能略高。一般情况下,要创建一个内容可变的字符串,建议优先考虑StringBuilder类。

九, 使用字符串时,new和""推荐使用哪种方式?


先看看 "hello" 和 new String("hello") 的区别:

当Java程序直接使用 "hello" 的字符串直接量时,JVM将会使用常量池来管理这个字符串;

当使用 new String("hello") 时,JVM会先使用常量池来管理 "hello" 直接量,再调用String类的构造器来创建一个新的String对象,新创建的String对象被保存在堆内存中。

显然,采用new的方式会多创建一个对象出来,会占用更多的内存,所以一般建议使用直接量的方式创建字符串。

十,说一说你对字符串拼接的理解

拼接字符串有很多种方式,其中最常用的有4种,下面列举了这4种方式各自适合的场景。

+ 运算符:如果拼接的都是字符串直接量,则适合使用 + 运算符实现拼接;

StringBuilder:如果拼接的字符串中包含变量,并不要求线程安全,则适合使用StringBuilder;

StringBuffer:如果拼接的字符串中包含变量,并且要求线程安全,则适合使用StringBuffer;

String类的concat方法:如果只是对两个字符串进行拼接,并且包含变量,则适合使用concat方法;

十一,两个字符串相加的底层是如何实现的?


如果拼接的都是字符串直接量,则在编译时编译器会将其直接优化为一个完整的字符串,和你直接写一个完整的字符串是一样的。

如果拼接的字符串中包含变量,则在编译时编译器采用StringBuilder对其进行优化,即自动创建StringBuilder实例并调用其append()方法,将这些字符串拼接在一起。

十二, String a = "abc"; ,说一下这个过程会创建什么,放在哪里?


JVM会使用常量池来管理字符串直接量。在执行这句话时,JVM会先检查常量池中是否已经存有"abc",若没有则将"abc"存入常量池,否则就复用常量池中已有的"abc",将其引用赋值给变量a。

十三, new String("abc") 是去了哪里,仅仅是在堆里面吗?


在执行这句话时,JVM会先使用常量池来管理字符串直接量,即将"abc"存入常量池。然后再创建一个新的String对象,这个对象会被保存在堆内存中。并且,堆中对象的数据会指向常量池中的直接量。


 十四,int和Integer有什么区别,二者在做==运算时会得到什么结果?


int是基本数据类型,Integer是int的包装类。二者在做==运算时,Integer会自动拆箱为int类型,然后再进行比较。届时,如果两个int值相等则返回true,否则就返回false。


十五,面向对象的三大特征是什么?


面向对象的程序设计方法具有三个基本特征:封装、继承、多态。其中,封装指的是将对象的实现细节隐藏起来,然后通过一些公用方法来暴露该对象的功能;继承是面向对象实现软件复用的重要手段,当子类继承父类后,子类作为一种特殊的父类,将直接获得父类的属性和方法;多态指的是子类对象可以直接赋给父类变量,但运行时依然表现出子类的行为特征,这意味着同一个类型的对象在执行同一个方法时,可能表现出多种行为特征。


十六,介绍一下Java的数据类型
 

Java数据类型包括基本数据类型和引用数据类型两大类。

基本数据类型有8个,可以分为4个小类,分别是整数类型(byte/short/int/long)、浮点类型(float/double)、字符类型(char)、布尔类型(boolean)。其中,4个整数类型中,int类型最为常用。2个浮点类型中,double最为常用。另外,在这8个基本类型当中,除了布尔类型之外的其他7个类型,都可以看做是数字类型,它们相互之间可以进行类型转换。

引用类型就是对一个对象的引用,根据引用对象类型的不同,可以将引用类型分为3类,即数组、类、接口类型。引用类型本质上就是通过指针,指向堆中对象所持有的内存空间,只是Java语言不再沿用指针这个说法而已。

扩展阅读

对于基本数据类型,你需要了解每种类型所占据的内存空间,面试官可能会追问这类问题:

byte:1字节(8位),数据范围是 -2^7 ~ 2^7-1。

short:2字节(16位),数据范围是 -2^15 ~ 2^15-1。

int:4字节(32位),数据范围是 -2^31 ~ 2^31-1。

long:8字节(64位),数据范围是 -2^63 ~ 2^63-1。

float:4字节(32位),数据范围大约是 -3.4*10^38 ~ 3.4*10^38。

double:8字节(64位),数据范围大约是 -1.8*10^308 ~ 1.8*10^308。

char:2字节(16位),数据范围是 \u0000 ~ \uffff。

boolean:Java规范没有明确的规定,不同的JVM有不同的实现机制。

对于引用数据类型,你需要了解JVM的内存分布情况,知道引用以及引用对象存放的位置,详见JVM部分的题目。


十七,说一说自动装箱、自动拆箱的应用场景
 

自动装箱、自动拆箱是JDK1.5提供的功能。

自动装箱:可以把一个基本类型的数据直接赋值给对应的包装类型;

自动拆箱:可以把一个包装类型的对象直接赋值给对应的基本类型;

通过自动装箱、自动拆箱功能,可以大大简化基本类型变量和包装类对象之间的转换过程。比如,某个方法的参数类型为包装类型,调用时我们所持有的数据却是基本类型的值,则可以不做任何特殊的处理,直接将这个基本类型的值传入给方法即可。


十八,如何对Integer和Double类型判断相等?
 

Integer、Double不能直接进行比较,这包括:

不能用==进行直接比较,因为它们是不同的数据类型;

不能转为字符串进行比较,因为转为字符串后,浮点值带小数点,整数值不带,这样它们永远都不相等;

不能使用compareTo方法进行比较,虽然它们都有compareTo方法,但该方法只能对相同类型进行比较。

整数、浮点类型的包装类,都继承于Number类型,而Number类型分别定义了将数字转换为byte、short、int、long、float、double的方法。所以,可以将Integer、Double先转为转换为相同的基本数据类型(如double),然后使用==进行比较。

Redis缓存类


十九,什么是缓存穿透 ? 怎么解决 ?

缓存穿透是指查询一个一定不存在的数据,如果从存储层查不到数据则不写
入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致
DB 挂掉。这种情况大概率是遭到了攻击。
解决方案的话,我们通常都会用布隆过滤器来解决它


你能介绍一下布隆过滤器吗?

布隆过滤器主要是用于检索一个元素是否在一个集合中。我们当时使用的是
redisson实现的布隆过滤器。
它的底层主要是先去初始化一个比较大数组,里面存放的二进制0或1。在一
开始都是0,当一个key来了之后经过3次hash计算,模于数组长度找到数据
的下标然后把数组中原来的0改为1,这样的话,三个数组的位置就能标明一
个key的存在。查找的过程也是一样的。
当然是有缺点的,布隆过滤器有可能会产生一定的误判,我们一般可以设置
这个误判率,大概不会超过5%,其实这个误判是必然存在的,要不就得增
加数组的长度,其实已经算是很划分了,5%以内的误判率一般的项目也能
接受,不至于高并发下压倒数据库

二十,什么是缓存击穿 ? 怎么解决 ?


缓存击穿的意思是对于设置了过期时间的key,缓存在某个时间点过期的时
候,恰好这时间点对这个Key有大量的并发请求过来,这些请求发现缓存过
期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能
会瞬间把 DB 压垮。
解决方案有两种方式:
第一可以使用互斥锁:当缓存失效时,不立即去load db,先使用如 Redis 的
setnx 去设置一个互斥锁,当操作成功返回时再进行 load db的操作并回设缓
存,否则重试get缓存的方法
第二种方案可以设置当前key逻辑过期,大概是思路如下:
①:在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前
key设置过期时间
②:当查询的时候,从redis取出数据后判断时间是否过期
③:如果过期则开通另外一个线程进行数据同步,当前线程正常返回数据,
这个数据不是最新
当然两种方案各有利弊:
如果选择数据的强一致性,建议使用分布式锁的方案,性能上可能没那么
高,锁需要等,也有可能产生死锁的问题
如果选择key的逻辑删除,则优先考虑的高可用性,性能比较高,但是数据
同步这块做不到强一致

二十一,什么是缓存雪崩 ? 怎么解决 ?


缓存雪崩意思是设置缓存时采用了相同的过期时间,导致缓存在某一时刻同
时失效,请求全部转发到DB,DB 瞬时压力过重雪崩。与缓存击穿的区别:
雪崩是很多key,击穿是某一个key缓存。
解决方案主要是可以将缓存失效时间分散开,比如可以在原有的失效时间基
础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重
复率就会降低,就很难引发集体失效的事件。


二十二,redis做为缓存,mysql的数据如何与redis进行同步呢?(双写一致
性)


就说我最近做的这个项目,里面有xxxx(根据自己的简历上
写)的功能,需要让数据库与redis高度保持一致,因为要求时效性比较高,
我们当时采用的读写锁保证的强一致性。
我们采用的是redisson实现的读写锁,在读的时候添加共享锁,可以保证读
读不互斥,读写互斥。当我们更新数据的时候,添加排他锁,它是读写,读
读都互斥,这样就能保证在写数据的同时是不会让其他线程读数据的,避免
了脏数据。这里面需要注意的是读方法和写方法上需要使用同一把锁才行。


二十三,这个排他锁是如何保证读写、读读互斥的呢?

其实排他锁底层使用也是setnx,保证了同时只能有一个线程操作
锁住的方法

二十四,你听说过延时双删吗?为什么不用它呢?

延迟双删,如果是写操作,我们先把缓存中的数据删除,然后更新
数据库,最后再延时删除缓存中的数据,其中这个延时多久不太好确定,在
延时的过程中可能会出现脏数据,并不能保证强一致性,所以没有采用它。


二十五,
1.redis做为缓存,数据的持久化是怎么做的?

在Redis中提供了两种数据持久化的方式:1、RDB 2、AOF

2.这两种持久化方式有什么区别呢?

RDB是一个快照文件,它是把redis内存存储的数据写到磁盘上,当
redis实例宕机恢复数据的时候,方便从RDB的快照文件中恢复数据。
AOF的含义是追加文件,当redis操作写命令的时候,都会存储这个文件中,
当redis实例宕机恢复数据的时候,会从这个文件中再次执行一遍命令来恢复
数据

3.这两种方式,哪种恢复的比较快呢?

RDB因为是二进制文件,在保存的时候体积也是比较小的,它恢复
的比较快,但是它有可能会丢数据,我们通常在项目中也会使用AOF来恢复
数据,虽然AOF恢复的速度慢一些,但是它丢数据的风险要小很多,在AOF
文件中可以设置刷盘策略,我们当时设置的就是每秒批量写入一次命令


二十六,Redis的数据过期策略有哪些 ?

在redis中提供了两种数据过期删除策略
第一种是惰性删除,在设置该key过期时间后,我们不去管它,当需要该key
时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。
第二种是 定期删除,就是说每隔一段时间,我们就对一些key进行检查,删
除里面过期的key
定期清理的两种模式:
SLOW模式是定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配
置文件redis.conf 的 hz 选项来调整这个次数
FAST模式执行频率不固定,每次事件循环会尝试执行,但两次间隔不低于2ms,
每次耗时不超过1ms
Redis的过期删除策略:惰性删除    +    定期删除两种策略进行配合使用。

 

(八股采自牛客,黑马等平台)

 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值