Mr. Cappuccino的第30杯咖啡——金三银四面试题之面试实战篇

1. Object类中有哪些方法?

registerNatives()、getClass()、hashCode()、equals()、clone()、toString()、notify()、notifyAll()、wait()、finalize()。

2. 为什么要把wait()放在Object类中,而不像sleep()方法一样放在Thread类中?

简单说:因为synchronized中的这把锁可以是任意对象,所以任意对象都可以调用wait()和notify();所以wait()和notify()属于Object。
专业说:因为这些方法在操作同步线程时,都必须要标识它们操作线程的锁,只有同一个锁上的被等待线程,可以被同一个锁上的notify()唤醒,不可以对不同锁中的线程进行唤醒。
也就是说,等待和唤醒必须是同一个锁。而锁可以是任意对象,所以可以被任意对象调用的方法是定义在Object类中。

3. notify()和notifyAll()的区别?

锁池:假设线程A已经拥有对象锁,线程B、C想要获取锁就会被阻塞,进入一个地方去等待锁的等待,这个地方就是该对象的锁池;
等待池:假设线程A调用某个对象的wait方法,线程A就会释放该对象锁,同时线程A进入该对象的等待池中,进入等待池中的线程不会去竞争该对象的锁。

notify()和notifyAll()的区别:

  1. notify()只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会;
  2. notifyAll()会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会;
4. 单向链表、双向链表、环形链表有什么区别?分别用在哪些场景中比较合适?

单向链表:只有一个指向下一个节点的指针。
优点:单向链表增加删除节点简单。遍历时候不会死循环;
缺点:只能从头到尾遍历。只能找到后继,无法找到前驱,也就是只能前进。
适用于节点的增加删除。

双向链表:有两个指针,一个指向前一个节点,一个后一个节点。
优点:可以找到前驱和后继,可进可退;
缺点:增加删除节点复杂,需要多分配一个指针存储空间。
适用于需要双向查找节点值的情况。

环形链表:环形链表的任意元素都有一个前驱和一个后继,所有数据元素在关系上构成逻辑上的环。环形链表是一种特殊的单链表,尾结点的指针指向首结点的地址。
应用场景:约瑟夫问题。

5. 如何避免死锁?

什么是死锁?
死锁是指多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。例如,在某一个计算机系统中只有一台打印机和一台输入 设备,进程P1正占用输入设备,同时又提出使用打印机的请求,但此时打印机正被进程P2 所占用,而P2在未释放打印机之前,又提出请求使用正被P1占用着的输入设备。这样两个进程相互无休止地等待下去,均无法继续执行,此时两个进程陷入死锁状态。

死锁产生的原因:

  1. 系统资源的竞争:系统资源的竞争导致系统资源不足,以及资源分配不当,导致死锁。
  2. 进程运行推进顺序不合适:进程在运行过程中,请求和释放资源的顺序不当,会导致死锁。

产生死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

如何避免死锁?

  1. 破坏”互斥”条件:系统里取消互斥、若资源一般不被一个进程独占使用,那么死锁是肯定不会发生的,但一般“互斥”条件是无法破坏的,因此,在死锁预防里主要是破坏其他三个必要条件,而不去涉及破坏“互斥”条件。
  2. 破坏“不可剥夺”条件:一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到 系统的资源列表中,可以被其他的进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行。
  3. 破坏”请求与保持条件“:第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源。第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源。
  4. 破坏“循环等待”条件:采用资源有序分配其基本思想是将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。

避免死锁的方式:

  1. 设置优先级;
  2. 设置超时时间(ReentrantLock接口中: boolean tryLock(long time, TimeUnit unit) );
  3. 根据特定的顺序获取锁;
  4. 尽量降低锁的粒度;
  5. 使用ConcurrentHashMap、ConcurrentLinkedQueue、AtomicBoolean等类实现原子操作,实际应用中简单方便且效率比Lock更高;
  6. 银行家算法;

如何检测死锁?

  1. jstack(使用jps查询pid,再通过jstack pid命令检测死锁);
  2. jconsole;
6. 谈谈线程的生命周期?

开始时:就绪状态,等待CPU调用后进入运行状态,运行过程中遇到阻塞事件,进入阻塞状态,等待阻塞事件结束后,重新进入就绪状态;如果没有阻塞事件,运行结束后,则进入结束状态。
在这里插入图片描述

7. 线程池有哪些核心的参数?谈谈线程池的工作原理?拒绝策略有哪些?默认是哪一种拒绝策略?

核心参数:
corePoolSize:核心线程数量,一直正在保持运行的线程;
maximumPoolSize:最大线程数,线程池允许创建的最大线程数;
keepAliveTime:超出corePoolSize后创建的线程的存活时间;
unit:keepAliveTime的时间单位;
workQueue:任务队列,用于保存待执行的任务;
threadFactory:线程池内部创建线程所用的工厂;
handler:任务无法执行时的处理器;

工作原理:

  1. 提交任务的时候比较核心线程数,如果当前任务数量小于核心线程数的情况下,则直接复用线程执行;
  2. 如果任务量大于核心线程数,则缓存到队列中;
  3. 如果缓存队列满了,且任务数小于最大线程数的情况下,则创建线程执行;
  4. 如果队列且最大线程数都满的情况下,则走拒绝策略;
    注意:最大线程数,在一定时间没有执行任务 则销毁避免浪费CPU内存;

拒绝策略:

  1. AbortPolicy 丢弃任务,抛运行时异常;
  2. CallerRunsPolicy 执行任务;
  3. DiscardPolicy 忽视,什么都不会发生;
  4. DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务;
  5. 实现RejectedExecutionHandler接口,可自定义处理器;

默认的拒绝策略:AbortPolicy

8. 谈谈Spring的生命周期?

简单来说,Spring Bean的生命周期只有四个阶段:实例化 Instantiation --> 属性赋值 Populate --> 初始化 Initialization --> 销毁 Destruction
具体来说,Spring Bean的生命周期包含下图的流程:
在这里插入图片描述

  1. 进入refresh()刷新方法;
  2. finishBeanFactoryInitialization(beanFactory);初始化所有单例对象;
  3. beanFactory.preInstantiateSingletons();初始化所有单例对象(非懒加载);
  4. getBean(beanName) → doGetBean() 先查询该对象是否有被初始化过,如果没有,则注册到IOC容器中;
  5. 如果对象是单例的话,调用createBean()方法创建对象;
  6. doCreateBean()创建IOC对象;
  7. createBeanInstance()使用Java反射机制实例化对象,实例化后的对象被封装在BeanWrapper对象中;
  8. populateBean()设置对象属性;
  9. initializeBean()执行初始化的方法(也可以自定义初始化方法);
  10. invokeAwareMethods()检查Aware相关接口并设置相关依赖;
  11. applyBeanPostProcessorsBeforeInitialization() 在初始化方法之前执行处理(增强);
  12. invokeInitMethods()调用自定义init方法(使用Java反射技术);
  13. applyBeanPostProcessorsAfterInitialization()在初始化方法之后执行处理(增强);
  14. 使用Bean对象;
  15. 销毁bean对象;

BeanNameAware:setBeanName();
BeanFactoryAware:setBeanFactory();
BeanPostProcessor:postProcessBeforeInitialization()/postProcessAfterInitialization()对bean对象自定义的初始化方法实现增强;
InitializingBean:afterPropertiesSet();
ApplicationContextAware:setApplicationContext();

9. 什么是Spring循环依赖问题?

在两个Bean对象相互依赖(A对象引用B对象,B对象引用A对象)且都是多例的情况下,会产生循环依赖问题。(单例的情况,Spring已经解决)

10. 如何解决循环依赖问题?

循环依赖问题在Spring中主要有三种情况:
(1)通过构造方法进行依赖注入时产生的循环依赖问题。
(2)通过setter方法进行依赖注入且是在多例(原型)模式下产生的循环依赖问题。
(3)通过setter方法进行依赖注入且是在单例模式下产生的循环依赖问题。
在Spring中,只有第(3)种方式的循环依赖问题被解决了,其他两种方式在遇到循环依赖问题时都会产生异常。这是因为:
第一种构造方法注入的情况下,在new对象的时候就会堵塞住了,其实也就是”先有鸡还是先有蛋“的历史难题。
第二种setter方法(多例)的情况下,每一次getBean()时,都会产生一个新的Bean,如此反复下去就会有无穷无尽的Bean产生了,最终就会导致OOM问题的出现。
Spring在单例模式下的setter方法依赖注入引起的循环依赖问题,主要是通过二级缓存和三级缓存来解决的,其中三级缓存是主要功臣。解决的核心原理就是:在对象实例化之后,依赖注入之前,Spring提前暴露的Bean实例的引用在第三级缓存中进行存储。

11. Spring一级缓存、二级缓存、三级缓存分别有什么作用?
  1. 一级缓存用于存放已经实例化、初始化完成的Bean(单例池 - singletonObjects);
  2. 二级缓存用于存放已经实例化,但未初始化的Bean.保证一个类多次循环依赖时仅构建一次保证单例(提前曝光早产bean池 - earlySingletonObjects);
  3. 三级缓存用于存放该Bean的BeanFactory,当加载一个Bean会先将该Bean包装为BeanFactory放入三级缓存(早期单例bean工厂池 - singletonFactories);
12. BeanFactory和FactoryBean的区别?

BeanFactory是个Factory,也就是IOC容器或对象工厂,FactoryBean是个Bean。在Spring中,所有的Bean都是由BeanFactory(也就是IOC容器)来进行管理的。但对FactoryBean而言,这个Bean不是简单的Bean,而是一个能生产或者修饰对象生成的工厂Bean,它的实现与设计模式中的工厂模式和修饰器模式类似。

13. 谈谈SQL语句中关键字的执行顺序?
(8) SELECT (9) DISTINCT <select_list>
(1) FROM <left_table>
(3) <join_type> JOIN <right_table>
(2) ON <join_condition>
(4) WHERE <where_condition>
(5) GROUP BY <group_by_list>
(6) WITH {CUBE|ROLLUP}
(7) HAVING <having_condition>
(10)ORDER BY <order_by_list>
(11)LIMIT <limit_number>
  1. FROM:对FROM左边的表和右边的表计算笛卡尔积,产生虚表VT1;
  2. ON:对虚拟表VT1进行ON筛选,只有那些符合条件的行才会被记录在虚拟表VT2中;
  3. JOIN:如果是OUT JOIN,那么将保留表中(如左表或者右表)未匹配的行作为外部行添加到虚拟表VT2中,从而产生虚拟表VT3;
  4. WHERE:对虚拟表VT3进行WHERE条件过滤,只有符合的记录才会被放入到虚拟表VT4;
  5. GROUP BY:根据GROUP BY子句中的列,对虚拟表VT4进行分组操作,产生虚拟表VT5;
  6. CUBE|ROLLUP:对虚拟表VT5进行CUBE或者ROLLUP操作,产生虚拟表VT6;
  7. HAVING:对虚拟表VT6进行 HAVING 条件过滤,只有符合的记录才会被插入到虚拟表VT7中;
  8. SELECT:执行SELECT操作,选择指定的列,插入到虚拟表VT8中;
  9. DISTINCT:对虚拟表VT8中的记录进行去重,产生虚拟表VT9;
  10. ORDER BY:将虚拟表VT9中的记录按照进行排序操作,产生虚拟表VT10;
  11. LIMIT:取出指定行的记录,产生虚拟表VT11,并将结果返回。
14. 如何对SQL查询语句进行优化?
  1. 全值匹配;
  2. 遵循最佳左前缀法则;
  3. 不在索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描;
  4. 存储引擎不能使用索引中范围条件右边的列(范围之后全失效,不包括本身)#若中间索引列用到了范围(>、<、like等),则后面的索引全失效;
  5. 尽量使用覆盖索引(只访问索引的查询(索引列包含查询列)),减少select *语句;
  6. 在使用(!=或者<>)的时候无法使用索引会导致全表扫描;
  7. is null,is not null 也无法使用索引;
  8. like以通配符开头(’$abc…’)mysql索引失效会变成全表扫描操作;(1.%放在后面,2.使用覆盖索引,查询字段必须是建立覆盖索引字段)
  9. 字符串不加单引号索引失效;
  10. 少用or,用它连接时很多情况下索引会失效;
15. 什么是泛型?

泛型,即“参数化类型”。泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

16. 策略模式和责任链模式有啥区别?

策略模式:
策略模式是对算法的包装,是把使用算法的责任和算法本身分割开来,委派给不同的对象管理,最终可以实现解决多重if判断问题。

  1. 环境(Context)角色:持有一个Strategy的引用。
  2. 抽象策略(Strategy)角色:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
  3. 具体策略(ConcreteStrategy)角色:包装了相关的算法或行为。

定义策略接口->实现不同的策略类->利用多态或其他方式调用策略

责任链模式:
定义:使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。其过程实际上是一个递归调用。

  1. 抽象处理者(Handler)角色:定义出一个处理请求的接口。如果需要,接口可以定义出一个方法以设定和返回对下家的引用。这个角色通常由一个Java抽象类或者Java接口实现。
  2. 具体处理者(ConcreteHandler)角色:具体处理者接到请求后,可以选择将请求处理掉,或者将请求传给下家。由于具体处理者持有对下家的引用,因此,如果需要,具体处理者可以访问下家。
17. Redis有哪些基本数据类型?有哪些应用场景?

基本数据类型:
String(最大容量512MB)、List、Hash、Set、Sorted Set。

应用场景:

  1. Token令牌的生成;
  2. 短信验证码Code;
  3. 缓存查询数据;
  4. 网页计数器;
  5. 分布式锁;
  6. 延迟操作;
18. Redis缓存穿透、缓存击穿、缓存雪崩分别是啥?怎么解决?

缓存穿透:缓存穿透是指使用不存在的key进行大量的高并发查询,导致缓存无法命中,每次请求都要都要穿透到后端数据库查询,使得数据库的压力非常大,甚至导致数据库服务压死;
解决方案:

  1. 接口层实现api限流、用户授权、id检查等 黑名单和白名单;
  2. 从缓存和数据库都取不到数据的话,一样将数据库空值放入缓存中,设置30s有效期避免使用同一个id对数据库攻击压力大;
  3. 使用布隆过滤器

缓存击穿:在高并发的情况下,当一个缓存key过期时,因为访问该key请求较大,多个请求同时发现缓存过期,因此对多个请求同时数据库查询、同时向Redis写入缓存数据,这样会导致数据库的压力非常大;
解决方案:

  1. 使用分布式锁
  2. 保证在分布式情况下,使用分布式锁保证对于每个key同时只允许只有一个线程查询到后端服务,其他没有获取到锁的权限,只需要等待即可;这种高并发压力直接转移到分布式锁上,对分布式锁的压力非常大。
  3. 使用本地锁,使用本地锁与分布式锁机制一样,只不过分布式锁适应于服务集群、本地锁仅限于单个服务使用。
  4. 软过过期,设置热点数据永不过期或者异步延长过期时间;

缓存雪崩:缓存雪崩指缓存服务器重启或者大量的缓存集中在某个时间段失效,突然给数据库产生了巨大的压力,甚至击垮数据库的情况。
解决思路:对不用的数据使用不同的失效时间,加上随机数

19. Redis做分布式锁有哪些缺点?

Redis集群数据同步采用异步的形式;
优点:写的效率比较高;
缺点:有可能存在数据不一致性问题;

Zookeeper集群数据同步采用同步的形式;
优点:保证每个节点的数据一致性;
缺点:写的效率比较低;

问题描述:Redis集群部署,JVM01连接主节点进行setnx操作,获取锁成功,此时主节点宕机了,主节点还没有异步将数据同步给从节点,Redis集群自动开启哨兵机制进行选举,选举出了一个新的主节点,由于此时的主节点没有同步到刚刚JVM01获取锁的数据,当JVM02再进行setnx操作时,仍然能成功获取锁。两个JVM同时获取锁,违背了分布式锁的原子性。

解决方案:

  1. 将Redis集群数据同步改为同步的形式;(缺点:效率偏低)
  2. 使用红锁;

红锁:
Redis的分布式锁算法采用红锁机制,红锁需要至少三个以上的Redis独立节点,这些节点
相互之间不需要存在主从之分,每个Redis保证独立即可。
获取锁原理:

  1. 客户端使用相同的key,再从所有的Redis节点获取锁;
  2. 客户端需要设置超时时间,连接Redis不成功的情况下立即切换到下一个Redis实例,防止一直阻塞;
  3. 客户端需要计算获取锁的总耗时,客户端至少要有N/2+1节点获取锁成功
    且总耗时时间小于锁的过期时间才能获取锁成功。
  4. 如果客户端最终获取锁失败,必须所有节点释放锁。

Redis集群在有主从之分的情况下容易产生脑裂问题;
Zookeeper集群采用过半机制,先天性避免脑裂问题;

20. 分别谈谈Redis哨兵机制和Cluster集群?

哨兵机制:
Redis的哨兵机制就是解决主从复制存在缺陷(选举问题),解决问题保证我们的Redis高可用,实现自动化故障发现与故障转移。
哨兵机制原理

  1. 哨兵机制每个10s时间只需要配置监听主节点就可以获取当前整个Redis集群的环境列表,采用info 命令形式。
  2. 哨兵不建议是单机的,最好每个Redis节点都需要配置哨兵监听。
  3. 哨兵集群原理是如何:多个哨兵都执行同一个主的master节点,订阅到相同都通道,有新的哨兵加入都会向通道中发送自己服务的信息,该通道的订阅者可以发现新哨兵的加入,随后相互建立长连接。
  4. Master的故障发现 单个哨兵会向主的master节点发送ping的命令,如果master节点没有及时的响应,哨兵会认为该master节点为“主观不可用状态”会发送给其他都哨兵确认该Master节点是否不可用,当前确认的哨兵节点数>=quorum(可配置),会实现重新选举。

Cluster集群:
传统Redis集群存在的问题:Redis哨兵集群模式,每个节点都保存全量同步数据,冗余的数据比较多;而在Redis Cluster模式中集群中采用分片集群模式,可以减少冗余数据,缺点就是构建该集群模式成本非常高。
Redis3.0开始官方推出了集群模式 RedisCluster,原理采用hash槽的概念,预先分配16384个卡槽,并且将该卡槽分配给具体服务的节点;通过key进行crc16(key)%16384 获取余数,余数就是对应的卡槽的位置,一个卡槽可以存放多个不同的key,从而将读或者写转发到该卡槽的服务的节点。 最大的优点:动态扩容、缩容。
在这里插入图片描述

21. ArrayList、LinkedList和Vector有什么区别?

共同点:有序,元素可以重复
ArrayList:基于数组实现,下标查询的时间复杂度为O(1),查询效率高,增删效率低(需要扩容),扩容是原来的1.5倍,线程不安全;
LinkedList:基于双向链表实现,下标查询的时间复杂度为O(log2n)(采用二分查找),查询效率低,增删效率高,线程不安全;
Vector:基于数组实现,下标查询的时间复杂度为O(1),查询效率高,增删效率低(需要扩容),扩容是原来的2倍,线程安全(synchronized同步锁);
补充:ArrayList采用数组存储,插入和删除的时间复杂度受位置影响,执行add(E e)方法的时候,时间复杂度为O(1),但是在指定位置插入元素执行add(int index, E element)方法时,时间复杂度则是O(n-i),需要将第i和第i个元素之后的(n-i)个元素向后移动一位;LinkedList采用双向链表存储,在头尾增删效率高,在中间位置增删效率低。ArrayList空间浪费主要体现在数组的结尾会预留一定容量的空间,而LinkedList空间浪费主要体现在每一个元素都会存放前驱结点和后继结点。ArrayList和Vector实现了RamdomAccess接口,表示其支持快速随机访问,而LinkedList不支持快速随机访问。

22. HashMap底层的数据结构是怎么样的?1.8版本为什么要用红黑树?

Java1.7底层实现:
基于数组+链表实现(Key和value封装成Entry对象)
Java1.8底层实现:
基于数组+链表+红黑树实现(Key和value封装成Entry对象)

在JDK1.7中,如果发生了hash冲突,则会将其存放在同一个链表中,当链表的长度过长时,查询效率非常低,链表的时间复杂度为O(n),从JDK1.8开始引入了红黑树,当数组容量>=64且链表长度>8,则会将链表转化成红黑树,红黑树的时间复杂度为O(logn),性能有所提升。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值