面试题-java高级(答案超详细)
java高级面试题,包含高并发、分布式、数据库底层原理、JVM、Spring底层原理等内容,最重要的是答案超详细
高并发
问:Java中的锁有几种?
答:公平锁/非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
对于ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock重新进入锁。
对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。
独享锁/共享锁
独享锁是指该锁一次只能被一个线程所持有。
共享锁是指该锁可被多个线程所持有。
对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
对于Synchronized而言,当然是独享锁。
互斥锁/读写锁
上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。
互斥锁在Java中的具体实现就是ReentrantLock
读写锁在Java中的具体实现就是ReadWriteLock
乐观锁/悲观锁
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。
从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
悲观锁在Java中的使用,就是利用各种锁。
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
自旋锁
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
典型的自旋锁实现的例子,可以参考自旋锁的实现
问:volatile关键字的如何保证内存可见性
答:volatile关键字的作用
保证内存的可见性
防止指令重排
注意:volatile并不保证原子性
内存可见性
ovolatile保证可见性的原理是在每次访问变量时都会进行一次刷新,因此每次访问都是主内存中最新的版本。所以volatile关键字的作用之一就是保证变量修改的实时可见性。
当且仅当满足以下所有条件时,才应该使用volatile变量
对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
该变量没有包含在具有其他变量的不变式中。
volatile使用建议
在两个或者更多的线程需要访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,没必要使用volatile。
由于使用volatile用蔽掉了JMM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。
volatile和synchronized区别
volatile不会进行加锁操作:
volatile变量是一种稍弱的同步机制在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。
volatile变量作用类似于同步变量读写操作:
从内存可见性的角度看,写入volatile变量相当于退出同步代码块,而读取volatile变量相当于进入同步代码块。
volatile不如synchronized安全:
在代码中如果过度依赖volatile变量来控制状态的可见性,通常会比使用锁的代码更脆弱,也更难以理解。仅当volatile变量能简化代码的实现以及对同步策路的验证时,才应该使用它。一般来说,用同步机制会更安全些。
volatile无法同时保证内存可见性和原则性:
加锁机制{即同步机制)既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性,原因是声明为volatile的简单变量如果当前值与该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不是原子操作:“count+ +"、 "count = count+1”。
问:介绍一下AQS
高手回答:
AQS全名:AbstractQueuedSynchronizer,是并发容器J.U.C(java.util.concurrent)下locks包内的一个类。它实现了一个FIFO(FirstIn、FisrtOut先进先出)的队列。底层实现的数据结构是一个双向链表。
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
问:synchronized的介绍、锁升级过程
高手回答:
synchronized是Java中的关键字,底层是JVM实现的一种同步锁,synchronized能同时保证可见性,原子性,有序性;
synchronized有三种方式来加锁,不同的修饰类型,代表锁的控制粒度:
● 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
● 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
● 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁
在 JDK 1.6之前,synchronized 还是一个重量级锁,是一个效率比较低下的锁,但是在JDK 1.6后,Jvm为了提高锁的获取与释放效率对(synchronized )进行了优化,引入了偏向锁和轻量级锁,从此以后锁的状态就有了四种(无锁、偏向锁、轻量级锁、重量级锁),并且四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级,也就是说只能进行锁升级(从低级别到高级别),不能锁降级(高级别到低级别),意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
偏向锁:
当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的ID,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁。如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,引入偏向锁是为了不存在线程竞争的情况下尽量减少不必要的加解锁。
轻量级锁:
当一个线程获取到该锁后,另一个线程也来获取该锁,这个线程并不会被直接阻塞,而是通过自旋来等待该锁被释放。自旋就是让线程执行一段无意义的循环。分为自适应自旋锁和固定次数自旋锁,前者是自旋次数位动态的,JVM通过之前这把锁的获得情况来自动的选择增加或者减少自旋次数直至阻塞。后者即固定次数自旋,超过则阻塞。
自旋锁设计的原因:Java的线程是映射到操作系统原生线程之上的,阻塞或唤醒线程要操作系统从用户态和核心态之间切换, 线程刚刚进入阻塞状态,这个锁就被其他线程释放了,则需要操作系统来唤醒,浪费资源。
重量级锁:由对象内置锁ObjectMonitor实现:
objectMonitor流程:
● 所有期待获得锁的线程,在锁已经被其它线程拥有的时候,这些期待获得锁的线程就进入了对象锁的entry set区域(监控区)。
● 所有曾经获得过锁,但是由于其它必要条件不满足而需要wait的时候,线程就进入了对象锁的wait set区域(待授权区) 。
● 在wait set区域的线程获得Notify/notifyAll通知的时候,随机的一个Thread(Notify)或者是全部的Thread(NotifyALL)从对象锁的wait set区域进入了entry set中。
● 在当前拥有锁的线程释放掉锁的时候,处于该对象锁的entryset区域的线程都会抢占该锁,但是只能有任意的一个Thread能取得该锁,而其他线程依然在entry set中等待下次来抢占到锁之后再执行。
问:讲讲synchronized锁升级流程
高手回答:
其实在 JDK 1.6之前,synchronized 还是一个重量级锁,是一个效率比较低下的锁,但是在JDK 1.6后,Jvm为了提高锁的获取与释放效率对(synchronized )进行了优化,引入了 偏向锁 和 轻量级锁 ,从此以后锁的状态就有了四种(无锁、偏向锁、轻量级锁、重量级锁),并且四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级。
jdk1.6之前的synchronized实现方式是 “阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长”,这也是在JDK6以前 synchronized效率低下的原因,JDK6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。
当一个线程去抢占锁的时候,锁会升级成偏向锁,此时共享资源加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距;
当多个线程去抢占锁的时候,此时一个线程会抢占到锁(线程会在栈中开辟一段空间来存储对象头中的MarkWord数据,而对象头会指向线程)然后执行同步代码块,而其他线程此时并不会直接进入到阻塞状态,而是通过自旋的方式去不断尝试抢占锁,自旋竞争,会消耗cpu,但是比起后面的重量级锁性能还是快很多,毕竟不会阻塞线程;
线程自旋抢占锁的次数并不是无限的,因为自旋也会消耗CPU的性能,当自旋到一定次数后还没拿到锁的话,线程就会讲锁的状态修改为重量级锁,并且阻塞。
数据库
问:a,b,c三个联合索引,若select * from table where b=?,c=? 走索引吗?如果select * from table where a=?,b=?,c=?走索引吗?如果select * from table where a=?b>?,c=?走索引吗?为什么?
答:首先,走不走索引不一定能完全我们自己控制,因为语句会走到优化器,优化器会去生成一条最优的执行计划。
第一个,where b = ? c=? ,联合索引是abc,不满足最左匹配原则,最左匹配原则是我的索引必须是最左边的联合索引字段开始
所以,第二个 where a=?,b=?,c=?是会走到联合索引的
select * from table where a=?b>?,c=? 会走到联合索引,但是不会所有的字段都会走索引,只有 a、b字段能走到索引,因为b是个范围查询,会导致下一个索引列失效。
问:为什么RR能重复读,RC不行? readAndView解决了幻读了吗,其存储的是什么?
答:不管是RC还是RR,mysql查询都会生成一个readAndView。
readAndView会记录当前所有未提交的事务ID,当前没提交的事务ID的最小ID( min_trx_id),以及下一个即将分配的事务ID(max_trx_id)。
每条数据都会有一个隐藏字段DB_TRX_ID; 通过db_trx_id与readAndView中的数据进行比较;规则为
- 如果数据的DB_TRX_ID 2. 如果数据的DB_TRX_ID >=max_trx_id, 那么表明修改数据的事务是在第一次快照之后修改提交的,不可见
- 如果 min_trx_id a.DB_TRX_ID如果在trx_ids中,表面在创建第一次快照之时还没提交,不可见
b.DB_TRX_ID如果不在trx_ids,表面在创建第一次快照之时已经提交,可见其中,RR只会在第一次查询的时候生成一个readAndView,RC每次查询都会生成一个readAndView。
根据以上的规则,也解决了幻读问题。
问:Mysql的事务隔离级别
答:隔离级别有RU、RC、RR、串行化,其中RU存在脏读、不可重复读、幻读问题、RC解决了脏读问题。
在InnoDB中、RR解决了脏读、不可重复读、幻读问题、串行化不存在并发,也就没有脏读、不可重复读、幻读问题。
其中innoDB中RR解决幻读是通过MVCC或者LBCC去解决,MVCC在读场景通过快照版本去解决幻读,LBCC通过加间隙锁去解决幻读
问:Mysql是如何保证事务的ACID的?
答:ACID就是事务的特性。既原子性(A) 一致性(C) 隔离性(I) 持久性(D)
其中undolog保证原子性,undo就是回滚日志,会记录数据修改之前的逻辑日志,一旦发生异常,就可以用undoLog来进行数据回滚
事务的隔离级别来保证隔离性 ,隔离级别有RU、RC、RR、串行化,其中RU存在脏读、不可重复读、幻读问题、RC解决了脏读问题。
在InnoDB中、RR解决了脏读、不可重复读、幻读问题、串行化不存在并发,也就没有脏读、不可重复读、幻读问题。
其中innoDB中RR解决幻读是通过MVCC或者LBCC去解决,MVCC在读场景通过快照版本去解决幻读,LBCC通过加间隙锁去解决幻读
Mysql的持久性是通过RedoLog和double write来保证数据写了就一定要做到,redolog就是恢复日志,为了性能,在内存也会有个redologbuffer内存区间,然后再跟磁盘交互,所以,redolog也会存在数据丢失的场景,如果要保证不丢失,必须要保证redologbuffer里的数据写到磁盘,才commit成功!!
一致性 就是我的数据要完整不被破坏 mysql中的AID,都是为了保证数据的一致性。
问:.数据库索引的优缺点以及什么时候数据库索引失效
答:●索引的特点
(可以加快数据库的检索連度
(降低数据库插入、修改删除等维护的連度
(只能创建在表上,不能创建到视图上
( 既可以直接创建又可以间接创建
(可以在优化隐藏中使用索弓|
(使用查询处理器执行5QL语句 ,在一个表上,- -次只能使用一个索引
●索引的优点
(创建唯一-性索引,保证数据库表中每一-行数据的唯- -性
( 大大加快数据的检索連度,这是创建索引的最主要的原因
(加連数据库表之 间的连接,特别是在实现数据的参考完整性方面特别有意义
(在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间
(通过使用索引,可以在查询中使用优化隐藏器, 提高系统的性能
●索引的缺点
(创建索引和维护索引|要耗费时间,这种时间随着数据量的增而增加)
(索引需要占用物理空间,除了数据表占用数据空间之外,每-个索引还要占-定的物理空间,如果建立聚簇索引,那么需要的空间就会更大)
(当对表中的数据进行增加口、 删除和修改的时候,索引也需要维护,降低数据维护的連度)
●索引分类
(直接创建索弓|和间接创建索引)
(昔通索弓|和唯一-性索引)
(单个索引和复合索弓|)
( 聚簇索弓|和非聚簇索引)
●索引失效
(如果条件中有or ,即使其中有条件带索引也不会使用(这就是问什么尽量少使用or的原因)
(对于多列索引,不是使用的第一 部分,则不会使用索引
(like查询是以 %开头)
(如果列类型是字符串 , 那一定要在条件中使用引号弓起来,否则不会使用索弓|)
(如果mysq|估计使用全表扫秒比使用索弓|快,则不适用索引)。
问:请你说下对InnoDB索引数据结构的理解?
高手回答:
InnoDB索弓|的数据结构用的是B+树。为什么要用B+?
InnoDB索引肯定会有1个聚集索引,聚集索引默认是主键,、然后是非空的唯一索引, 最后是隐藏列rowid。
聚集索引的存储方式为叶子节点有完整的数据,而非叶子节点,只存有索引值。
那么每页存的数据也就更多,内存跟磁盘交互的单位为页。每页的数据越多,那么就能减少跟磁盘的交互次
数,整体上提升速度。
同时,因为真实数据都在叶子节点,所以sq|语句的查询路径都是一样长。 查询稳定。
为什么不用二叉查找树?因为二叉树会有斜树的情况出现,会退化成链表,不够平衡。
为什么不用红黑树?同样的,路数比较少,深度会随着数据量的提升而提升。速度会越来越慢。同时也不够平
衡。
为什么不用B tree?B树跟B+最大的一个区别,B树每个节点都有真实数据,那么每页存的数据就越少,查询数
据跟磁盘的交互也就越多,同时,索弓|树的高度也就越高,查询的链路也会越长,整体查询会慢。还有每个节
点都有真实数据。查询数据就不稳定,有些在索引第-层就能查到,有些要查到
索引最后一层。
分布式
缓存
问:用LRU算法设计一个缓存。(会用什么数据结构)
答:LRU即最久未使用。我们可以使用链表 与hash,其中链表来保证数据操作的性能,hash保证查询的性能
每次访问或者操作时,我们必须放在最前面,可以用hash找到之前的位置,然后移动到链表表头
7.Mysql的事务隔离级别
隔离级别有RU、RC、RR、串行化,其中RU存在脏读、不可重复读、幻读问题、RC解决了脏读问题。
在InnoDB中、RR解决了脏读、不可重复读、幻读问题、串行化不存在并发,也就没有脏读、不可重复读、幻读问题。
其中innoDB中RR解决幻读是通过MVCC或者LBCC去解决,MVCC在读场景通过快照版本去解决幻读,LBCC通过加间隙锁去解决幻读
问:Jedis 与 Redisson 对比有什么优缺点?
答:Jedis 是 Redis 的 Java 实现的客户端, 其 API 提供了比较全面的 Redis 命令的支 持;Redisson 实现了分布式和可扩展的 Java 数据结构,和 Jedis 相比,功能较为简单, 不支持字符串操作, 不支持排序、事务、管道、分区等 Redis 特性。Redisson 的宗旨是 促进使用者对 Redis 的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑 上。
问:怎么理解 Redis 事务?
答:1) 事务是一个单独的隔离操作: 事务中的所有命令都会序列化、按顺序地执行。事 务在执行的过程中, 不会被其他客户端发送来的命令请求所打断。
2) 事务是一个原子操作: 事务中的命令要么全部被执行, 要么全部都不执行。
问:假如 Redis 里面有 1 亿个key,其中有 10w 个key 是以 某个固定的已知的前缀开头的,如果将它们全部找出来?
答:使用 keys 指令可以扫出指定模式的 key 列表。
对方接着追问: 如果这个 redis 正在给线上的业务提供服务, 那使用 keys 指令会有什么 问题? 这个时候你要回答 redis 关键的一个特性:redis 的单线程的。keys 指令会导致线程阻塞 一段时间, 线上服务会停顿, 直到指令执行完毕, 服务才能恢复。这个时候可以使用 scan 指令, scan 指令可以无阻塞的提取出指定模式的 key 列表, 但是会有一定的重复 概率, 在客户端做一次去重就可以了, 但是整体所花费的时间会比直接用 keys 指令长。
JVM
问:平时有JVM调优的经验吗?
答:有过,这里简单的记住几个常见的例子,调优是没有一个完全正确的答案的,这里面试官考的是三点:第一点是看你是否真的对JVM有研究过,而不是单纯是背出来的;第二点是看你的解决问题的思路是否清晰,只要你能分析出一两个问题就很好了;第三点就是看你之前的工作经验了。
JVM调优我给一个大体的步骤,但是不同的问题也要具体看待:
1)分析GC日志,通过printGCDetails参数打印出GC的日志,然后通过工具查看日志中的吞吐量,GC的次数是否过多,平均停顿时间、最大停顿时间是否过长等等;通过GC日志要初步定位问题,比如说如果GC次数过多是否是因为堆内存确实过小、是否存在大对象不能被回收、是否存在死循环、是否因为停顿时间设置过小而导致等等;
2)通过上一步分析出问题可能的原因,然后再去查看dump文件中的堆内存的使用详情,通过这一步再一次定位问题;如果是跟CPU有关的问题就要通过jstack查看栈信息。
3)根据上面两步定位到的问题,然后尝试调整相关参数,再观察调整之后的结果
4)重复第三步
问:CMS和G1讲讲流程
高手回答:
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。采用的是"标记-清除算法",整个过程分为4步;
(1)初始标记,标记GC Roots能直接关联到的对象 Stop The World-- ->速度很快
(2)并发标记,就是从GC Roots开始找到它能引用的所有其它对象的过程
(3)重新标记,Stop The World 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间要短。
(4)并发清除,在整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,因此,从总体上看,CMS收集器的内存回收过程是与用户线程一起并发执行的。
G1收集器的工作过程也可以分为四步:
初始标记、并发标记、最终标记跟CMS的前三步基本一致;
筛选回收,对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间制定回收计划
运维
问:CPU使用率过高应该如何排查?
答:1.使用top 定位到占用CPU高的进程PIDtop 指令通过ps aux | grep PID命令
2.获取线程信息,并找到占用CPU高的线程ps -mp 进程ID -o THREAD,tid,time | sort -rn
3.将需要的线程ID转换为16进制格式printf “%x\n” tid
4.打印线程的堆栈信息jstack pid |grep tid -A 30
问:给你一个dump文件,知道怎么分析吗?
答:一般我们会通过参数进行配置,使得在出现OOM的时候自动打印dump文件,然后可以通过jvisualvm或者MAT等工具分析dump文件,从dump文件中我们能得到堆的使用情况,GC的情况以及在堆里面具体是哪个对象导致的OOM等等
spring
1 什么是Spring框架?Spring框架有哪些主要模块?
Spring框架是一个为Java应用程序开发提供综合、广泛的基础性支持的Java平台。Spring帮助开发者解决了开发中基础性的问题,使得开发人员可以专注于应用程序的开发。Spring框架本身也是按照设计模式精心打造,这使得我们可以在开发环境中安心地集成Spring框架,不必担心Spring是如何在后台工作的。
Spring大约18个基本模块,大致分为4类;分别是核心模块、AOP、数据访问、Web模块、测试模块。
核心模块包括:core、beans、context、context-support、expression共5个模块;
AOP模块包括:aop、aspects、instrument共3个模块;
数据访问模块包括:jdbc、tx、orm、oxm共4个模块;
Web模块包括:web、webmvc、websocket、webflux共4个模块;
集成测试模块:test模块。
问:SpringBoot启动过程
回答思路:SpringBoot启动时通过执行main方法中的SpringApplication.run方法去启动的,在run方法中调用了SpringApplication的构造方法,在该构造方法中加载了META-INFA\spring.factories文件配置的ApplicationContextInitializer的实现类和ApplicationListenerr的实现类,ApplicationContextInitializer 这个类当springboot上下文Context初始化完成后会调用。ApplicationListener当springboot启动时事件change后都会触发。
SpringApplication实例构造完之后会调用它的run方法,在run方法中作了以下几步重要操作:
- 获取事件监听器SpringApplicationRunListener类型,并且执行starting()方法
- 准备环境,并且把环境跟spring上下文绑定好,并且执行environmentPrepared()方法
- 创建上下文,根据项目类型创建上下文
- 执行spring的启动流程扫描并且初始化单实列bean
同时通过@SpringBootApplication注解将ClassPath路径下所有的META-INF\spring.factories文件中的EnableAutoConfiguration实例注入到IOC容器中
问:Bean的生命周期?
回答思路:bean的生命周期就是一个创建bean的过程,主要分为4个步骤,实例化,属性注入,初始化,销毁
高手回答:spring的bean的生命周期主要是创建bean的过程,一个bean的生命周期主要是4个步骤,实例化,属性注入,初始化,销毁,但是对于一些复杂的bean的创建,spring会在bean的生命周期中开放很多的接口,可以让你加载bean的时候对bean做一些改变,因此spring的bean的生命周期总共有以下几步:
首先在spring中有一些特殊的bean会介入到其他bean的声明周期当中去,所以一个普通的bean的声明周期为:
- 实现了BeanFactoryPostProcessor接口的bean,在加载其他的bean的时候,也会调用这个bean的postProcessBeanFactory方法,可以在这个步骤去对bean中的属性去赋值。设置年龄初始化18等等。
- 实现了InstantiationAwareBeanPostProcessor接口的bean,会在实例化bean之前调用postProcessBeforeInstantiation方法
- 然后在对bean进行实例化
- 对bean进行属性注入
- 对bean进行初始化,在初始化中,包含了以下几个步骤:
1)实现了BeanFactoryAware接口,会先调用setBeanFactory方法
2)实现了BeanNameAware接口,会先调用setBeanName方法
3)实现了BeanPostProcessor接口,会先调用postProcessBeforeInitialization方法
3)实现了InitializingBean接口,会调用afterPropertiesSet方法
4)然后在进行aop后置处理,通过实现BeanPostProcessor接口,在postProcessAfterInitialization方法中进行动态代理 - 销毁
当然还有一些其他的步骤,在此就不一一列举了,bean的生命周期总共有18步。
问:Spring Aop的理解和使用?
参考思路:Aop面向切面编程,在spring中,通过配置切面类和切点,切面类中主要有前置通知方法,后置通知方法,异常通知方法以及环绕通知方法,切面类中的方法的逻辑就是你要织入代码的逻辑,然后你需要判断哪些类需要进行前面切面,所以就要配置切入点,可以使用xml方式,也可以使用注解的方法。
实现方式主要是通过动态代理给目标类生成代理对象,如果目标类实现了接口就使用jdk代理,如果没有实现接口使用cglib代理,如果配置了优先级,则默认使用cglib代理。在spring容器启动后,就会去扫描哪些需要进行aop的类,这些类有哪些切面(切面是可以配置多个的),然后给目标类的每个方法都绑定一个相应的执行链,并放入到一个map中,当你调用时,会走代理类,然后通过索引加递归的方式去调用切面类前置通知方法,目标类方法,后置通知方法这样的顺序去调用。
使用:可以拦截目标类,在方法的前后织入日志打印逻辑,包括spring事务开启也是使用的aop,还有动态数据源切换等场景都是可以使用aop。如果配置多个切面,则要指定执行顺序,通过设置sort的大小来指定,sort值越小,越先执行,越后结束,sort值越大,越后执行,越先结束,就相当于一个同心圆。事务sort值默认是int类型的最大值。
如何实现热部署
1、使用调试模式Debug实现热部署
此种方式为最简单最快速的一种热部署方式,运行系统时使用Debug模式,无需装任何插件即可,但是无发对配置文件,方法名称改变,增加类及方法进行热部署,使用范围有限。
2、spring-boot-devtools
在 Spring Boot 项目中添加 spring-boot-devtools依赖即可实现页面和代码的热部署。
如下:
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-devtools
</artifactId>
</dependency>
此种方式的特点是作用范围广,系统的任何变动包括配置文件修改、方法名称变化都能覆盖,但是后遗症也非常明显,它是采用文件变化后重启的策略来实现了,主要是节省了我们手动点击重启的时间,提高了实效性,在体验上回稍差。spring-boot-devtools 默认关闭了模版缓存,如果使用这种方式不用单独配置关闭模版缓存。
问:对spring ioc的理解和使用?
高手回答:
以前没有spring的时候,我们需要得到一个对象,都是自己主动去new一个对象,然后通过set方法给对象注入属性,但是这种动作其实是一个重复的动作,所以spring提供ioc的容器解决方案,在容器启动的时候就把许多需要实例化和属性注入的bean都提前做好并放入到一个map中存储起来。这就是控制反转,原来的控制全在用户,现在的控制权完全交给了容器,在bean实例化后,通过反射对属性进行依赖注入
有两种使用方式,一种是xml的方式,一种是注解的方式。
xml的加载方式,首先在spring的xml中通过bean标签配置我们需要注入的bean,当扫描到所有的bean后,首先把bean包装成BeanDefinition,放入到list中,然后循环这个list去创建bean,创建bean的步骤为实例化->属性注入->初始化->aop,然后最终放入到spring的一级缓存中保存起来。
注解方式是通过在类上面加上spring的注解去扫描,比如controller,service等,后续步骤和xml基本一样。
问:对spring事务的理解
参考思路:spring的事务开启方式有两种,一个是声明式事务,一个是编程式事务,声明式事务是通过添加Transaction注解的方式开启事务,一般加在类或者方法上,事务控制粒度比较大,但使用上比较方便,编程式事务通过TransactionTemplate,控制事务的粒度小,但是代码侵入性比较强。使用spring aop实现。
spring事务提供了7中传播机制:
- REQUIRED(必须的):是默认的传播机制,如果B方法中调用A,如果B中有事务,则A无论是否开启事务都会用B的事务,任何地方出现异常A和B都回滚
- REQUIRES_NEW(需要新的):每次都会开启一个新的事务,外面事务回滚,里面事务不会回滚
- SUPPORTS(1): 如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行,完全依赖最外层事务
- MANDATORY(强制性的):必须运行在事务里面
- NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
- NEVER:以非事务方式执行,如果当前存在事务,则抛出异常
- NESTED:开启新事务,提交事务依赖于外层事务,如果外层事务回滚,则里面事务也回滚
spring事务还提供了四种隔离级别: - DEFAULT(-1):数据库默认的隔离级别
- READ_UNCOMMITTED(1):读未提交 ru,会导致脏读
- READ_COMMITTED(2):读已提交 rc 避免脏读,允许不可重复读和幻读
- REPEATABLE_READ(4):可重复读 rr 避免脏读,不可重复读,允许幻读,innodb存储引擎解决了幻读
- SERIALIZABLE:串行化
从上到下,隔离级别越来越高,并发性能就越来越差,spring事务的本质还是数据库的事务,如果数据库不支持事务,spring的事务也就没有了意义
什么是Spring框架,Spring框架有哪些主要模块
Spring框架是一个为Java应用程序开发提供综合、广泛的基础性支持的Java平台。Spring帮助开发者解决了开发中基础性的问题,使得开发人员可以专注于应用程序的开发。Spring框架本身也是按照设计模式精心打造的,这使得我们可以在开发环境中安心地集成Spring框架,不必担心Spring是如何在后台工作的。
使用Spring框架能带来哪些好处
下面列举了一些使用Spring框架带来的主要好处。
(1)Dependency Injection(DI)使得构造器和JavaBean properties文件中的依赖关系一目了然。
(2)与EJB容器相比较,IoC容器更加趋向于轻量级。这样一来使用IoC容器在有限的内存和CPU资源的情况下进行应用程序的开发和发布就变得十分有利。
(3)Spring并没有闭门造车,Spring利用了已有的技术,比如ORM框架、logging框架、J2EE、Quartz和JDK Timer,以及其他视图技术。
(4)Spring框架是按照模块的形式来组织的。由包和类的编号就可以看出其所属的模块,开发者只需选用需要的模块即可。
(5)要测试一个用Spring开发的应用程序十分简单,因为测试相关的环境代码都已经囊括在框架中了。更加简单的是,利用JavaBean形式的POJO类,可以很方便地利用依赖注入来写入测试数据。
(6)Spring的Web框架也是一个精心设计的Web MVC框架,为开发者在Web框架的选择上提供了一个除主流框架(比如Struts)和过度设计的、不流行Web框架以外的选择。
(7)Spring提供了一个便捷的事务管理接口,适用于小型的本地事务处理(比如在单DB的环境下)和复杂的共同事务处理(比如利用JTA的复杂DB环境)。
什么是控制反转(IoC),什么是依赖注入
IOC,Inversion of Control,控制反转,指将对象的控制权转移给Spring框架,由 Spring 来负责控制对象的生命周期(比如创建、销毁)和对象间的依赖关系。
最直观的表达就是,以前创建对象的时机和主动权都是由自己把控的,如果在一个对象中使用另外的对象,就必须主动通过new指令去创建依赖对象,使用完后还需要销毁(比如Connection等),对象始终会和其他接口或类耦合起来。而 IOC 则是由专门的容器来帮忙创建对象,将所有的类都在 Spring 容器中登记,当需要某个对象时,不再需要自己主动去 new 了,只需告诉 Spring 容器,然后 Spring 就会在系统运行到适当的时机,把你想要的对象主动给你。也就是说,对于某个具体的对象而言,以前是由自己控制它所引用对象的生命周期,而在IOC中,所有的对象都被 Spring 控制,控制对象生命周期的不再是引用它的对象,而是Spring容器,由 Spring 容器帮我们创建、查找及注入依赖对象,而引用对象只是被动的接受依赖对象,所以这叫控制反转。
请解释Spring中的IoC容器
Spring中的org.springframework.beans包和org.springframework.context包构成了Spring IoC容器的基础。BeanFactory接口提供了一个先进的配置机制,使得任何类型的对象的配置都成为可能。ApplicationContex接口对BeanFactory(是一个子接口)进行了扩展,在BeanFactory的基础上添加了其他功能,比如与Spring的AOP更容易集成,也提供了处理Message Resource的机制(用于国际化),以及事件传播及应用层的特别配置,比如针对Web应用的WebApplicationContext。
什么是依赖注入(DI)
IoC 的一个重点就是在程序运行时,动态的向某个对象提供它所需要的其他对象,这一点是通过DI(Dependency Injection,依赖注入)来实现的,即应用程序在运行时依赖 IoC 容器来动态注入对象所需要的外部依赖。而 Spring 的 DI 具体就是通过反射实现注入的,反射允许程序在运行的时候动态的生成对象、执行对象的方法、改变对象的属性
4.7 Spring如何解决循环依赖问题
循环依赖问题在Spring中主要有三种情况:
(1)通过构造方法进行依赖注入时产生的循环依赖问题。
(2)通过setter方法进行依赖注入且是在多例(原型)模式下产生的循环依赖问题。
(3)通过setter方法进行依赖注入且是在单例模式下产生的循环依赖问题。
在Spring中,只有第(3)种方式的循环依赖问题被解决了,其他两种方式在遇到循环依赖问题时都会产生异常。
因为:
第一种构造方法注入的情况下,在new对象的时候就会堵塞住了,其实也就是”先有鸡还是先有蛋“的历史难题。
第二种setter方法(多例)的情况下,每一次getBean()时,都会产生一个新的Bean,如此反复下去就会有无穷无尽的Bean产生了,最终就会导致OOM问题的出现。
Spring在单例模式下的setter方法依赖注入引起的循环依赖问题,主要是通过二级缓存和三级缓存来解决的,其中三级缓存是主要功臣。解决的核心原理就是:在对象实例化之后,依赖注入之前,Spring提前暴露的Bean实例的引用在第三级缓存中进行存储。
BeanFactory和ApplicationContext有什么区别
BeanFactory 可以理解为含有Bean集合的工厂类。BeanFactory 包含了Bean的定义,以便在接收到客户端请求时将对应的Bean实例化。BeanFactory还能在实例化对象时生成协作类之间的关系。此举将Bean自身从Bean客户端的配置中解放出来。BeanFactory还包含Bean生命周期的控制,调用客户端的初始化方法(Initialization Method)和销毁方法(Destruction Method)。从表面上看,ApplicationContext如同BeanFactory一样具有Bean定义、Bean关联关系的设置及根据请求分发Bean的功能。但ApplicationContext在此基础上还提供了其他功能。
(1)提供了支持国际化的文本消息。
(2)统一的资源文件读取方式。
(3)已在监听器中注册的Bean的事件。
以下是三种较常见的 ApplicationContext 实现方式。
(1)ClassPathXmlApplicationContext:从ClassPath的XML配置文件中读取上下文,并生成上下文定义。应用程序上下文从程序环境变量中取得。ApplicationContext context = new ClassPathXmlApplicationContext(“application.xml”);
(2)FileSystemXmlApplicationContext :由文件系统中的XML配置文件读取上下文。ApplicationContext context = new FileSystemXmlApplicationContext(“application.xml”);
(3)XmlWebApplicationContext:由Web应用的XML文件读取上下文。
Spring中用到了哪些设计模式
Spring中使用了大量的设计模式,下面列举了一些比较有代表性的设计模式。
(1)工厂模式:Spring使用工厂模式,通过BeanFactory和ApplicationContext来创建对象
(2)单例模式:Bean默认为单例模式
(3)策略模式:例如Resource的实现类,针对不同的资源文件,实现了不同方式的资源获取策略
(4)代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术
(5)模板方法:可以将相同部分的代码放在父类中,而将不同的代码放入不同的子类中,用来解决代码重复的问题。比如RestTemplate, JmsTemplate, JpaTemplate
(6)适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式,Spring MVC中也是用到了适配器模式适配Controller
(7)观察者模式:Spring事件驱动模型就是观察者模式的一个经典应用。
(8)桥接模式:可以根据客户的需求能够动态切换不同的数据源。比如我们的项目需要连接多个数据库,客户在每次访问中根据需要会去访问不同的数据库
在Spring中如何更有效地使用JDBC
使用Spring JDBC可以使得资源管理及错误处理的代价减小。开发人员只需通过statements和queries语句从数据库中存取数据。Spring通过模板类能更有效地使用JDBC,也就是所谓的JdbcTemplate。
在Spring中可以注入null或空字符串吗
完全可以。
web
问:cookie和session的区别
答:●session在服务器端Cookie在客户端(浏览器)
●session 的运行依赖session id,而session id是存在cookie中的,也就是说,如果浏览器禁用了cookie , 同时session也会失效(但是可以通过其它方式实现,比如在url中传递session _id )
●session 可以放在文件.数据库、或内存中都可以。
●用户验证这种场合一般会用 session
●cookie不是很安全,别人可以分析存放在本地的COOKI E并进行COOKIE欺骗考虑到安全应当使用session.
●session会在一 定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能考虑到减轻服务器性能方面,应当使用COOKIE.
●单个cookie保存的数据不能超过4K ,很多浏览器都限制个站点最多保存20个cookie。
微服务
问:限流策略有哪些,滑动窗口算法和令牌桶区别,使用场景
参考思路:限流算法常用的几种实现方式有如下四种:计数器、滑动窗口、漏桶和令牌桶;
● 计数器:
○ 思想:在固定时间窗口内对请求进行计数,与阀值进行比较判断是否需要限流,一旦到了时间临界点,将计数器清零。
○ 问题:计数器算法存在“时间临界点”缺陷。比如每一分钟限制100个请求,可以在00:00:00-00:00:58秒里面都没有请求,在00:00:59瞬间发送100个请求,这个对于计数器算法来是允许的,然后在00:01:00再次发送100个请求,意味着在短短1s内发送了200个请求,如果量更大呢,系统可能会承受不住瞬间流量,导致系统崩溃
● 滑动窗口:
○ 思想:滑动窗口算法将一个大的时间窗口分成多个小窗口,每次大窗口向后滑动一个小窗口,并保证大的窗口内流量不会超出最大值,这种实现比固定窗口的流量曲线更加平滑。
○ 问题:没有根本解决固定窗口算法的临界突发流量问题
● 漏桶:
○ 思想:漏桶算法是首先想象有一个木桶,桶的容量是固定的。当有请求到来时先放到木桶中,处理请求的worker以固定的速度从木桶中取出请求进行相应。如果木桶已经满了,直接返回请求频率超限的错误码或者页面
○ 适用场景:漏桶算法是流量最均匀的限流实现方式,一般用于流量“整形”。例如保护数据库的限流,先把对数据库的访问加入到木桶中,worker再以db能够承受的qps从木桶中取出请求,去访问数据库。
○ 问题:木桶流入请求的速率是不固定的,但是流出的速率是恒定的。这样的话能保护系统资源不被打满,但是面对突发流量时会有大量请求失败,不适合电商抢购和微博出现热点事件等场景的限流。
● 令牌桶:
○ 思想:令牌桶是反向的"漏桶",它是以恒定的速度往木桶里加入令牌,木桶满了则不再加入令牌。服务收到请求时尝试从木桶中取出一个令牌,如果能够得到令牌则继续执行后续的业务逻辑。如果没有得到令牌,直接返回访问频率超限的错误码或页面等,不继续执行后续的业务逻辑。
○ 适用场景:适合电商抢购或者微博出现热点事件这种场景,因为在限流的同时可以应对一定的突发流量。如果采用漏桶那样的均匀速度处理请求的算法,在发生热点时间的时候,会造成大量的用户无法访问,对用户体验的损害比较大。
设计模式
问:访问者模式是什么东西,解决什么问题?
回答思路:访问者模式是一种分离对象数据结构与行为的方法,通过这种分离,可以为一个已存在的类增加新的操作而无须为它们进行修改,在spring中就有对这个设计模式的实现案例,在java中我们会通过注解@Value通过占位符对类中的属性赋值,而且是解析的Properties 文件中的值映射到类的成员变量上,只需要修改Properties文件。在spring中每个对象都会被解析成BeanDefinition ,然后访问者模式中,会用Spring 的 BeanDefinitionVisitor 用来访问 BeanDefinition,访问的具体调用就是BeanDefinitionVisitor.visitBeanDefinition(bd);封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作。于要更新的表有状态字段,并且刚好要更新状态字段的这种特殊情况,并非所有场景都适用。
Java
四种引用讲讲,分别使用在哪些地方
高手回答:
java根据其生命周期的长短又将引用类型分为强引用、软引用、弱引用、虚引用;
强引用:new一个对象就是强引用,例如 Object obj = new Object();当JVM的内存空间不足时,宁愿抛出OutOfMemoryError使得程序异常终止也不愿意回收具有强引用的存活着的对象;
软引用的生命周期比强引用短一些。软引用是通过SoftReference类实现的。当JVM认为内存空间不足时,就会去试图回收软引用指向的对象;软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收! 这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。
弱引用是通过WeakReference类实现的,它的生命周期比软引用还要短。在GC的时候,不管内存空间足不足都会回收这个对象,也同样适用于内存敏感的缓存。如果一个对象是偶尔的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么应该用 Weak Reference 来记住此对象。或者想引用一个对象,但是这个对象有自己的生命周期,你不想介入这个对象的生命周期,这时候就应该用弱引用,这个引用不会在对象的垃圾回收判断中产生任何附加的影响。
虚引用,是通过PhantomReference类实现的。任何时候可能被GC回收,就像没有引用一样;无法通过虚引用访问对象的任何属性或者函数。虚引用仅仅只是提供了一种确保对象被finalize以后来做某些事情的机制。
问:一个对象是多少字节?在堆区都存了什么东西?对象头里面都有啥?
高手回答:
对象在内存中的存储的布局可以分为三块区域:对象头(Header),实例数据(Instance Data)和对齐填充(Padding);对象头由 Markword + 类指针kclass(该指针指向该类型在方法区的元类型) 组成;普通对象头在32位系统上占用8bytes,64位系统上占用16bytes。64位机器上,数组对象的对象头占用24个字节,启用压缩之后占用16个字节。
MarkWord用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。在64位的虚拟机(未开启压缩指针)为64bit。
kclass存储的是该对象所属的类在方法区的地址,所以是一个指针,默认Jvm对指针进行了压缩,用4个字节存储,如果不压缩就是8个字节。 关于Compressed Oops的知识,大家可以自行查阅相关资料来加深理解。
实例数据部分就是成员变量的值,其中包含父类的成员变量和本类的成员变量。也就是说,除去静态变量和常量值放在方法区,非静态变量的值是随着对象存储在堆中的。
用于确保对象的总长度为8字节的整数倍。HotSpot要求对象的总长度必须是8字节的整数倍。由于对象头一定是8字节的整数倍,但实例数据部分的长度是任意的。因此需要对齐补充字段确保整个对象的总长度为8的整数倍。
问:MyBatis只需要声明接口即可调用SQL是怎么做到的?JDK动态代理和CGLib代理区别?
参考思路:mybatis是通过对dao接口进行代理,在启动mybatis的时候,会去加载在mybtais配置文件中配置的mapper文件,mapper文件中的每个sql语句都封装成一个statement,然后通过mapper文件中的namespace去得到到接口然后给他生成代理的工厂类放入到map中,注入mapper接口类时从该map中拿出对接口代理的对象,在代理对象中的invoke方法中除了object的方法,其他的所有方法都直接去找到相应的statment,然后去执行sql语句;
Jdk动态代理的类必须要实现接口,并且不能是final修饰的类,方法不能是非public的方法,在生成代理类时快,只生成一个代理文件,生成的代理类回去实现目标类实现的接口,运行时通过反射调用目标类的方法,调用时慢,cglib是使用asm框架来生成代理类,目标类无须实现接口,生成的代理类会继承目标类,不能是final修饰的类,方法不能是非public,由于生成了几个类文件,所以生成时慢,之所以有几个类文件,因为cglib在生成代理类的同时,会为目标类的每个方法都生成一个相应的index,通过index直接定位到方法,直接调用,所以调用时快,在spring中会判断如果有接口就使用jdk代理,如果没有接口就使用cglib代理。
MyBatis
问:MyBatis插件原理。使用mybtais插件如何数据脱敏?
高手回答:
在mybtis中对外提供了4大对象供开发者拦截,分别是Excutor:查看mybatis的sql执行过程,ParameterHandler修改他的参数,ResultSetHandler拦截返回值,StatementHandler进行分页,开发者自己定义一个类,实现Interceptor接口,实现intercept方法,在类上通过@Intercepts注解和@Signature选择拦截哪个类中的哪个方法,比如:@Intercepts(@Signature(type = ResultSetHandler.class, method = “handleResultSets”, args = Statement.class)),然后那自定义拦截器的类配置到mybatis的配置文件中。在加载mybatis的时候会去在执行到ResultSetHandler的时候生成拦截器器的链,后面去调用会走拦截器
数据脱敏我们就需要去拦截ResultSetHandler对象修改返回值,在这个里面可以去做数据脱敏处理,可以通过反射获取对象的属性,判断哪些字段需要进行脱敏处理,如果需要脱敏处理,直接通过filed.setAccessibe(true)暴力反问,修改属性值
HashMap
- HashMap底层实现
分JDK1.7和JDK1.8来答
在JDK1.7时,HashMap的底层数据结构是 数组+链表
在JDK1.8时,HashMap的底层数据结构是 数组+链表+红黑树
- JDK1.8中HashMap的put()和get()操作的过程
put操作:
①首先判断数组是否为空,如果数组为空则进行第一次扩容(resize)
②根据key计算hash值并与上数组的长度-1(int index = key.hashCode()&(length-1))得到键值对在数组中的索引。
③如果该位置为null,则直接插入
④如果该位置不为null,则判断key是否一样(hashCode和equals),如果一样则直接覆盖value
⑤如果key不一样,则判断该元素是否为红黑树的节点,如果是,则直接在红黑树中插入键值对
⑥如果不是红黑树的节点,则就是链表,遍历这个链表执行插入操作,如果遍历过程中若发现key已存在,直接覆盖value即可。
如果链表的长度大于等于8且数组中元素数量大于等于阈值64,则将链表转化为红黑树,(先在链表中插入再进行判断)
如果链表的长度大于等于8且数组中元素数量小于阈值64,则先对数组进行扩容,不转化为红黑树。
⑦插入成功后,判断数组中元素的个数是否大于阈值64(threshold),超过了就对数组进行扩容操作。
get操作:
①计算key的hashCode的值,找到key在数组中的位置
②如果该位置为null,就直接返回null
③否则,根据equals()判断key与当前位置的值是否相等,如果相等就直接返回。
④如果不等,再判断当前元素是否为树节点,如果是树节点就按红黑树进行查找。
⑤否则,按照链表的方式进行查找。
HashMap的更多更详细的底层原理可以参考HashMap底层原理深度解析(源码剖析)
-
HashMap的扩容机制
数组的初始容量为16,而容量是以2的次方扩充的,一是为了提高性能使用足够大的数组,二是为了能使用位运算代替取模运算(据说提升了5-8倍)
数组是否需要扩充是通过负载因子判断的,如果当前元素个数为数组容量的0.75时,就会扩充数组。这个0.75就是默认的负载因子,可由构造器传入。我们也可以设置大于1的负载因子,这样数组就不会扩充,牺牲性能,节省内存
为了解决碰撞,数组中的元素是单向链表类型。当链表长度达到一个阈值(7或8),会将链表转换成红黑树提高性能,而当链表长度缩小到另一个阈值时(6),又会将红黑树转换回单向链表提高性能
对于第三点补充说明,检查链表长度转换成红黑树之前,还会先检测当前数组是否到达一个阈值(64),如果没有到达这个容量,会放弃转换,先去扩充数组。所以上面也说了链表长度的阈值是7或8,因为会有一次放弃转换的操作
4.HashMap的初始容量为什么是16?
减少hash碰撞(2n ,16=24)
需要在效率和内存使用上做一个权衡,这个值既不能太小,也不能太大
防止分配过小频繁扩充
防止分配过大浪费资源 -
HashMap为什么每次扩容都以2的整数次幂进行扩容?
因为Hashmap计算存储位置时,使用了(n - 1) & hash。只有当容量n为2的幂次方,n-1的二进制会全为1,位运算时可以充分散列,避免不必要的哈希冲突,所以扩容必须2倍就是为了维持容量始终为2的幂次方。 -
HashMap的扩容因子为什么是0.75?
当负载因子为1.0时,意味着只有当hashMap装满之后才会进行扩容,虽然空间利用率有大的提升,但是这就会导致大量的hash冲突,使得查询效率变低。
当负载因子为0.5或者更低的时候,hash冲突降低,查询效率提高,但是由于负载因子太低,导致原来只需要1M的空间存储信息,现在用了2M的空间。最终结果就是空间利用率太低。
负载因子是0.75的时候,这是时间和空间的权衡,空间利用率比较高,而且避免了相当多的Hash冲突,使得底层的链表或者是红黑树的高度也比较低,提升了空间效率。
- HashMap扩容后会重新计算Hash值吗?
①JDK1.7
JDK1.7中,HashMap扩容后,所有的key需要重新计算hash值,然后再放入到新数组中相应的位置。
②JDK1.8
在JDK1.8中,HashMap在扩容时,需要先创建一个新数组,然后再将旧数组中的数据转移到新数组上来。
此时,旧数组中的数据就会根据(e.hash & oldCap),数据的hash值与扩容前数组的长度进行与操作,根据结果是否等于0,分为2类。
1.等于0时,该节点放在新数组时的位置等于其在旧数组中的位置。
2.不等于0时,该节点在新数组中的位置等于其在旧数组中的位置+旧数组的长度。
- HashMap中当链表长度大于等于8时,会将链表转化为红黑树,为什么是8?
如果 hashCode 分布良好,也就是 hash 计算的结果离散好的话,那么红黑树这种形式是很少会被用到的,因为各个值都均匀分布,很少出现链表很长的情况。在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,当长度为 8 的时候,概率仅为 0.00000006。这是一个小于千万分之一的概率,通常我们的 Map 里面是不会存储这么多的数据的,所以通常情况下,并不会发生从链表向红黑树的转换。
通俗点讲就是put进去的key进行计算hashCode时 只要选择计算hash值的算法足够好(hash碰撞率极低),从而遵循泊松分布,使得桶中挂载的bin的数量等于8的概率非常小,从而转换为红黑树的概率也小,反之则概率大。
- HashMap为什么线程不安全?
1.在JDK1.7中,当并发执行扩容操作时会造成死循环和数据丢失的情况。
在JDK1.7中,在多线程情况下同时对数组进行扩容,需要将原来数据转移到新数组中,在转移元素的过程中使用的是头插法,会造成死循环。
2.在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。
如果线程A和线程B同时进行put操作,刚好这两条不同的数据hash值一样,并且该位置数据为null,所以这线程A、B都会通过判断,将执行插入操作。
假设一种情况,线程A进入后还未进行数据插入时挂起,而线程B正常执行,从而正常插入数据,然后线程A获取CPU时间片,此时线程A不用再进行hash判断了,问题出现:线程A会把线程B插入的数据给覆盖,发生线程不安全。
- 为什么HashMapJDK1.7中扩容时要采用头插法,JDK1.8又改为尾插法?
JDK1.7的HashMap在实现resize()时,新table[ ]的列表队头插入。
这样做的目的是:避免尾部遍历。
避免尾部遍历是为了避免在新列表插入数据时,遍历到队尾的位置。因为,直接插入的效率更高。
对resize()的设计来说,本来就是要创建一个新的table,列表的顺序不是很重要。但如果要确保插入队尾,还得遍历出链表的队尾位置,然后插入,是一种多余的损耗。直接采用队头插入,会使得链表数据倒序。
JDK1.8采用尾插法是避免在多线程环境下扩容时采用头插法出现死循环的问题。
- HashMap是如何解决哈希冲突的?
拉链法(链地址法)
为了解决碰撞,数组中的元素是单向链表类型。当链表长度大于等于8时,会将链表转换成红黑树提高性能。
而当链表长度小于等于6时,又会将红黑树转换回单向链表提高性能。
- HashMap为什么使用红黑树而不是B树或平衡二叉树AVL或二叉查找树?
1.不使用二叉查找树
二叉排序树在极端情况下会出现线性结构。例如:二叉排序树左子树所有节点的值均小于根节点,如果我们添加的元素都比根节点小,会导致左子树线性增长,这样就失去了用树型结构替换链表的初衷,导致查询时间增长。所以这是不用二叉查找树的原因。
2.不使用平衡二叉树
平衡二叉树是严格的平衡树,红黑树是不严格平衡的树,平衡二叉树在插入或删除后维持平衡的开销要大于红黑树。
红黑树的虽然查询性能略低于平衡二叉树,但在插入和删除上性能要优于平衡二叉树。
选择红黑树是从功能、性能和开销上综合选择的结果。
3.不使用B树/B+树
HashMap本来是数组+链表的形式,链表由于其查找慢的特点,所以需要被查找效率更高的树结构来替换。
如果用B/B+树的话,在数据量不是很多的情况下,数据都会“挤在”一个节点里面,这个时候遍历效率就退化成了链表。
- HashMap和Hashtable的异同?
①HashMap是?线程安全的,Hashtable是线程安全的。
Hashtable 内部的?法基本都经过 synchronized 修饰。
②因为线程安全的问题,HashMap要?Hashtable效率??点。
③HashMap允许键和值是null,而Hashtable不允许键或值是null。
HashMap中,null 可以作为键,这样的键只有?个,可以有?个或多个键所对应的值为 null。
HashTable 中 put 进的键值只要有?个 null,直接抛出 NullPointerException。
④ Hashtable默认的初始??为11,之后每次扩充,容量变为原来的2n+1。
HashMap默认的初始??为16,之后每次扩充,容量变为原来的2倍。
⑤创建时如果给定了容量初始值,那么 Hashtable 会直接使?你给定的??,? HashMap 会将其扩充为2的幂次???。
⑥JDK1.8 以后的 HashMap 在解决哈希冲突时当链表?度?于等于8时,将链表转化为红?树,以减少搜索时间。Hashtable没有这样的机制。
Hashtable的底层,是以数组+链表的形式来存储。
⑦HashMap的父类是AbstractMap,Hashtable的父类是Dictionary
相同点:都实现了Map接口,都存储k-v键值对。
- HashMap和HashSet的区别?
HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码?常?常少,因为除了 clone() 、 writeObject() 、 readObject() 是 HashSet ??不得不实现之外,其他?法都是直接调? HashMap 中的?法)
①HashMap实现了Map接口,HashSet实现了Set接口
②HashMap存储键值对,HashSet存储对象
③HashMap调用put()向map中添加元素,HashSet调用add()方法向Set中添加元素。
④HashMap使用键key计算hashCode的值,HashSet使用对象来计算hashCode的值,在hashCode相等的情况下,使用equals()方法来判断对象的相等性。
⑤HashSet中的元素由HashMap的key来保存,而HashMap的value则保存了一个静态的Object对象。
- HashSet和TreeSet的区别?
相同点:HashSet和TreeSet的元素都是不能重复的,并且它们都是线程不安全的。
不同点:
①HashSet中的元素可以为null,但TreeSet中的元素不能为null
②HashSet不能保证元素的排列顺序,TreeSet支持自然排序、定制排序两种排序方式
③HashSet底层是采用哈希表实现的,TreeSet底层是采用红黑树实现的。
④HashSet的add,remove,contains方法的时间复杂度是 O(1),TreeSet的add,remove,contains方法的时间复杂度是 O(logn)
HashSet底层是基于HashMap实现的,存入HashSet中的元素实际上由HashMap的key来保存,而HashMap的value则存储了一个静态的Object对象。
value中的值都是统一的一个private static final Object PRESENT = new Object();
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐



所有评论(0)