目录
- 1.数据库的三大范式
- 2.事务四个特性
- 3.知道多少种索引,分别讲讲
- 4.主键索引和唯一索引的区别
- 5.索引失效的场景
- 6.数据库的日志知道哪些,分别讲讲
- 7.redis的数据结构和应用场景
- 8.缓存击穿是怎么产生的,解决方案
- 9.redis中key的过期策略
- 10.redis内存淘汰策略
- 11.JDK和JRE有什么区别
- 12.java基础数据类型有哪些
- 13.java的各种权限修饰符和范围
- 14.抽象类和接口的区别
- 15.String和StringBuilder和StringBuffer的区别
- 16.Synchronized的底层原理的话,你说说锁升级
- 17.其他锁还知道哪些
- 18.线程的创建方法
- 19.线程的生命周期
- 20.sleep和wait的区别
- 21.notify和notifyAll的区别
- 22.ArrayList和LinkedList的区别,他们的查询效率谁快,为什么
- 23.HashMap的底层原理
- 24.HashMap多线程的时候会出现什么情况,要怎么解决
- 25.List线程安全的子类了解多少种
- 26.JVM的内存结构
- 27.栈和堆他们存储速度上谁快?
- 28.说下GC,常见的回收算法
- 29.堆的结构
- 30.类加载机制
- 31.双亲委派是什么,为什么这么设计?
- 32.说下OSI七层模型
- 33.HTTP和HTTPS的区别
- 34.TLS的握手过程
- 35.对称加密和非对称加密的区别
- 36.mybatis的#和$区别
- 37.mybatis的二级缓存是什么,分别的作用范围
- 38.Spring是什么
- 39.动态代理有哪两种,分别怎么实现的知道吗
1.数据库的三大范式
第一范式1NF核心原则:属性不可分割
第二范式2NF核心原则:不能存在部分函数依赖
第三范式3NF核心原则:不能存在传递依赖
2.事务四个特性
ACID
原子性(atomicity)
原子性是指事务是一个不可分割的工作单位,要么全部提交,要么全部失败回滚。
一致性(consistency)
根据定义,一致性是指事务执行前后,数据从一个 合法性状态 变换到另外一个 合法性状态 。这种状态是 语义上 的而不是语法上的,跟具体的业务有关。
隔离型(isolation)
事务的隔离性是指一个事务的执行 不能被其他事务干扰 ,即一个事务内部的操作及使用的数据对 并发 的其他事务是隔离的,并发执行的各个事务之间不能互相干扰
持久性(durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是 永久性的 ,接下来的其他操作和数据库故障不应该对其有任何影响。
持久性是通过 事务日志 来保证的。日志包括了 重做日志 和 回滚日志 。
3.知道多少种索引,分别讲讲
普通索引: 用表中的普通列构建的索引,没有任何限制;
唯一索引: 唯一索引列的值必须唯一,但允许有空值。如果是联合索引,则列值的联合必须唯一;
主键索引: 是一种特殊的唯一索引,根据主键建立索引,不允许重复,不允许空值;
全文索引: 通过过建立倒排索引,快速匹配文档的方式。MySQL 5.7.6 之前仅支持英文,MySQL 5.7.6 之后支持中文;
组合索引: 又叫联合索引。用多个列组合构建的索引,这多个列中的值不允许有空值。可以在创建表的时候指定,也可以修改表结构。
4.主键索引和唯一索引的区别
一个表中可以有多个唯一性索引,但只能有一个主键。
主键列不允许空值,而唯一性索引列允许空值。
5.索引失效的场景
1.最佳左前缀法则
结论:过滤条件要使用索引必须按照索引建立时的顺序,依次满足,一旦跳过某个字段,索引后面的字段都无法被使用。
2.计算、函数导致索引失效
3.范围条件右边的列索引失效
4.不等于(!= 或者<>)索引失效
5.is not null无法使用索引,is null可使用索引
6.like以通配符%开头索引失效
6.数据库的日志知道哪些,分别讲讲
如果不熟悉可以看这篇
MySQL 日志:undo log、redo log、binlog 有什么用?
bin log
1.作用
MySQL的bin log日志是用来记录MySQL中增删改时的记录日志。
当你的一条sql操作对数据库中的内容进行了更新,就会增加一条bin log日志。查询操作不会记录到bin log中。
bin log最大的用处就是进行主从复制,以及数据库的恢复。
主从复制:在Master端开启binlog,然后将binlog发送到各个Slave端,Slave端重放binlog从而达到主从数据一致。
数据恢复:通过使用mysqlbinlog工具来恢复数据。
2.刷盘时机
对于InnoDB存储引擎而言,只有在事务提交时才会记录biglog,此时记录还在内存中,那么biglog是什么时候刷到磁盘中的呢?mysql通过sync_binlog参数控制biglog的刷盘时机,取值范围是0-N:
1、sync_binlog=0 的时候,表示每次提交事务binlog不会马上写入到磁盘,而是先写到page cache,相对于磁盘写入来说写page cache要快得多,不过在Mysql 崩溃的时候会有丢失日志的风险。
2、sync_binlog=1 的时候,表示每次提交事务都会执行 fsync 写入到磁盘 ;
3、sync_binlog的值大于1 的时候,表示每次提交事务都 先写到page cach,只有等到积累了N个事务之后才fsync 写入到磁盘,同样在此设置下Mysql 崩溃的时候会有丢失N个事务日志的风险。
很显然三种模式下,sync_binlog=1 是强一致的选择,选择0或者N的情况下在极端情况下就会有丢失日志的风险,具体选择什么模式还是得看系统对于一致性的要求。
3.日志格式
logbin格式:
- binlog_format=STATEMENT(默认):数据操作的时间,同步时不一致 每一条会修改数据的sql语句会记录到binlog中。优点是并不需要记录每一行的数据变化,减少了binlog日志量,节约IO,提高性能。缺点是在某些情况下会导致 master-slave 中的数据不一致( 如sleep()函数, last_insert_id(),以及user-defined functions(udf)等会 出 现 问题)
- binlog_format=ROW:批量数据操作时,效率低 不记录每条sql语句的上下文信息,仅需记录哪条数据被修改了,修改成什么样 了。而且不会出 现某些特定情况下的存储过程、或function、或trigger的调用和触发无法被正确复制的 问题。缺 点是会产生大量的日志,尤其是alter table的时候会让日志暴涨。
- binlog_format=MIXED:是以上两种level的混合使用,有函数用ROW,没函数用STATEMENT,但是无法识别系统变量
redo log
1.为什么需要redo log
事务的四大特性里面有一个是持久性,具体来说就是只要事务提交成功,那么对数据库做的修改就被永久保存下来了,不可能因为任何原因再回到原来的状态。那么mysql是如何保证持久性的呢?最简单的做法是在每次事务提交的时候,将该事务涉及修改的数据页全部刷新到磁盘中
2.基本概念
redo log包括两部分:一个是内存中的日志缓冲(redo log buffer),另一个是磁盘上的日志文件(redo log file)。mysql每执行一条DML(Data Manipulation Language增删改)语句,先将记录写入redo log buffer,后续某个时间点再一次性将多个操作记录写到redo log file。这种先写日志,再写磁盘的技术就是MySQL里经常说到的WAL(Write-Ahead Logging)预写日志 技术。
3.作用
redo log是一种基于磁盘的数据结构,用来在MySQL宕机情况下将不完整的事务执行数据纠正,redo日志记录事务执行后的状态。
当事务开始后,redo log就开始产生,并且随着事务的执行不断写入redo log file中。redo log file中记录了xxx页做了xx修改的信息,我们都知道数据库的更新操作会在内存中先执行,最后刷入磁盘。
redo log就是为了恢复更新了内存但是由于宕机等原因没有刷入磁盘中的那部分数据。
3.刷盘时机
mysql支持三种将redo log buffer写入redo log file的时机,可以通过innodb_flush_log_at_trx_commit参数配置,各参数值含义如下:
参数值 | 含义 |
---|---|
取值0 | 每秒(一秒钟内提交的事务)写入磁盘 每秒触发一次缓存日志回写磁盘操作,并调用操作系统fsync刷新IO缓存。 |
取值1 | 有事务提交就立即刷盘 每次提交事务都立即调用操作系统fsync刷新IO缓存。 |
取值2 | 每次事务提交 都写给操作系统 由系统接管什么时候写入磁盘 每次都把redo log写到系统的page cache中,由系统接管什么时候写入磁盘 |
innodb_flush_log_at_trx_commit = 1:实时写,实时刷
这种策略会在每次事务提交之前,每次都会将数据从redo log刷到磁盘中去,理论上只要磁盘不出问题,数据就不会丢失。
innodb_flush_log_at_trx_commit = 0:延迟写,延迟刷
每秒(一秒钟内提交的事务)写入磁盘 每秒触发一次缓存日志回写磁盘操作,并调用操作系统fsync刷新IO缓存。当系统崩溃,会丢失1秒钟的数据。
innodb_flush_log_at_trx_commit = 2:实时写,延迟刷
这种策略在事务提交之前会把redo log写到os cache中,但并不会实时地将redo log刷到磁盘,而是会每秒执行一次刷新磁盘操作。
这种情况下如果MySQL进程挂了,操作系统没挂的话,操作系统还是会将os cache刷到磁盘,数据不会丢失
但如果MySQL所在的服务器挂掉了,也就是操作系统都挂了,那么os cache也会被清空,数据还是会丢失。
undo log
1.作用
undo log主要用来回滚到某一个版本,是一种逻辑日志。
数据库事务四大特性中有一个是原子性,具体来说就是 原子性是指对数据库的一系列操作,要么全部成功,要么全部失败,不可能出现部分成功的情况。实际上,原子性底层就是通过undo log实现的。undo log主要记录了数据的逻辑变化
undo log记录的是修改之前的数据,比如:当delete一条记录时,undolog中会记录一条对应的insert记录,从而保证能恢复到数据修改之前。在执行事务回滚的时候,就可以通过undo log中的记录内容并以此进行回滚。
undo log还可以提供多版本并发控制下的读取(MVCC)。
7.redis的数据结构和应用场景
String
应用场景
- 比如抖音无限点赞某个视频或者商品,点一下加一次
- 是否喜欢的文章(阅读数:只要点击了rest地址,直接可以使用incr key命令增加一个数字1,完成记录数字。)
Hash
应用场景
- JD购物车早期
List
应用场景 - 微信公众号订阅的消息
- 商品评论列表
Set
应用场景
- 微信抽奖小程序
- 微博好友关注社交关系
- QQ内推可能认识的人
Zset
应用场景
- 抖音热搜
8.缓存击穿是怎么产生的,解决方案
缓存击穿
大量的请求同时查询一个 key 时,此时这个key正好失效了,就会导致大量的请求都打到数据库上面去
简单说就是热点key突然失效了,暴打mysql
方案1:对于访问频繁的热点key,干脆就不设置过期时间
互斥独占锁防止击穿
方案2:互斥独占锁防止击穿
多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个 互斥锁来锁住它。
其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。
方案3:定时轮询,互斥更新,差异失效时间
举一个案例例子
QPS上1000后导致可怕的缓存击穿
解决方法
定时轮询,互斥更新,差异失效时间
相当于B穿了一件防弹衣
我们先缓存B,然后在缓存A,设置B的过期时间要比A的长
我们先查询A,如果A为空,我们再查询B
9.redis中key的过期策略
如果不熟悉可以看这篇
Redis过期删除策略和内存淘汰策略
立即删除
Redis不可能时时刻刻遍历所有被设置了生存时间的key,来检测数据是否已经到达过期时间,然后对它进行删除。
立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放。但是立即删除对cpu是最不友好的。因为删除操作会占用cpu的时间,如果刚好碰上了cpu很忙的时候,比如正在做交集或排序等计算的时候,就会给cpu造成额外的压力,让CPU心累,时时需要删除,忙死。。。。。。。
这会产生大量的性能消耗,同时也会影响数据的读取操作。
总结:对CPU不友好,用处理器性能换取存储空间
惰性删除
数据到达过期时间,不做处理。等下次访问该数据时,
如果未过期,返回数据 ;
发现已过期,删除,返回不存在。
惰性删除策略的缺点是,它对内存是最不友好的。
如果一个键已经过期,而这个键又仍然保留在redis中,那么只要这个过期键不被删除,它所占用的内存就不会释放。
在使用惰性删除策略时,如果数据库中有非常多的过期键,而这些过期键又恰好没有被访问到的话,那么它们也许永远也不会被删除(除非用户手动执行FLUSHDB),我们甚至可以将这种情况看作是一种内存泄漏–无用的垃圾数据占用了大量的内存,而服务器却不会自己去释放它们,这对于运行状态非常依赖于内存的Redis服务器来说,肯定不是一个好消息
定期删除
定期删除策略是前两种策略的折中:
定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。
周期性轮询redis库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度
特点1:CPU性能占用设置有峰值,检测频度可自定义设置
特点2:内存压力不是很大,长期占用内存的冷数据会被持续清理
总结:周期性抽查存储空间 (随机抽查,重点抽查)
总结
Redis服务器实际使用的是惰性删除和定期删除两种策略:通过配合使用这两种删除策略,服务器可以很好地在合理使用CPU时间和避免浪费内存空间之间取得平衡。
10.redis内存淘汰策略
如果不熟悉可以看这篇
Redis过期删除策略和内存淘汰策略
1.内存淘汰策略
有哪些
- noeviction: 不会驱逐任何key
- allkeys-lru: 对所有key使用LRU算法进行删除
- volatile-lru: 对所有设置了过期时间的key使用LRU算法进行删除
- allkeys-random: 对所有key随机删除
- volatile-random: 对所有设置了过期时间的key随机删除
- volatile-ttl: 删除马上要过期的key
- allkeys-lfu: 对所有key使用LFU算法进行删除
- volatile-lfu: 对所有设置了过期时间的key使用LFU算法进行删除
总结一下怎么记住这8个
2 * 4 得8
2个维度
过期键中筛选
所有键中筛选
4个方面
LRU
LFU
random
ttl
你平时用哪一种
平常使用allkeys-lru
系统默认的是noeviction
11.JDK和JRE有什么区别
JRE顾名思义是java运行时环境
JDK顾名思义是java开发工具包
如果你需要运行java程序,只需安装JRE就可以了。如果你需要编写java程序,需要安装JDK。
12.java基础数据类型有哪些
类型名称 | 字节空间 | 使用场景 |
---|---|---|
byte | 1个字节 | 存储字节数据 |
short | 2个字节 | 兼容性考虑 |
int | 4个字节 | 存储普通整数 |
long | 8个字节 | 存储长整数 |
float | 4个字节 | 存储浮点数 |
double | 8个字节 | 存储双精度浮点数 |
char | 2个字节 | 存储一个字节 |
boolean | 1个字节 | 存储逻辑变量 |
13.java的各种权限修饰符和范围
权限修饰符 | 同一个类 | 同一个包 | 不同包的子类 | 不同包的非子类 |
---|---|---|---|---|
private | √ | |||
default | √ | √ | ||
protected | √ | √ | √ | |
public | √ | √ | √ | √ |
14.抽象类和接口的区别
1、语法层面上的区别
- 抽象类可以有方法实现,而接口的方法中只能是抽象方法(Java 8 之后接口方法可以有默认实现);
- 抽象类中的成员变量可以是各种类型的,接口中的成员变量只能是public static final类型;
- 接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法(Java 8之后接口可以有静态方法);
- 一个类只能继承一个抽象类,而一个类却可以实现多个接口。
2、设计层面上的区别
- 抽象层次不同。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口只是对类行为进行抽象。继承抽象类是一种"是不是"的关系,而接口实现则是 "有没有"的关系。如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是具备不具备的关系,比如鸟是否能飞。继承抽象-
- 类的是具有相似特点的类,而实现接口的却可以不同的类。
15.String和StringBuilder和StringBuffer的区别
1.可变性
- String内部的value值是final修饰的,所以它是不可变类。所以每次修改String的值,都会产生一个新的对象。
- StringBuffer和StringBuilder是可变类,字符串的变更不会产生新的对象。
2.线程安全性
- String是不可变类,所以它是线程安全的。
- StringBuffer是线程安全的,因为它每个操作方法都加了synchronized同步关键字。
- StringBuilder不是线程安全的,所以在多线程环境下对字符串进行操作,应该使用StringBuffer,否则使用StringBuilder
3.性能方面
- String的性能是最的低的,因为不可变意味着在做字符串拼接和修改的时候,需要重新创建新的对象以及分配内存。
- 其次是StringBuffer要比String性能高,因为它的可变性使得字符串可以直接被修改。
- 最后是StringBuilder,它比StringBuffer的性能高,因为StringBuffer加了同步锁。
4.存储方面
- String存储在字符串常量池里面
- StringBuffer和StringBuilder存储在堆内存空间。
16.Synchronized的底层原理的话,你说说锁升级
重量级锁,假如锁的竞争比较激烈的话,性能下降
Java5之前,用户态和内核态之间的切换
synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的
Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁
- 偏向锁,就是直接把当前锁偏向于某个线程,简单来说就是通过CAS修改偏向锁标记,这种锁适合同一个线程多次去申请同一个锁资源并且没有其他线程竞争的场景。
- 轻量级锁也可以称为自旋锁,基于自适应自旋的机制,通过多次自旋重试去竞争锁。自旋锁优点在于它避免避免了用户态到内核态的切换带来的性能开销。
Synchronized引入了锁升级的机制之后,如果有线程去竞争锁:
首先,synchronized会尝试使用偏向锁的方式去竞争锁资源,如果能够竞争到偏向锁,表示加锁成功直接返回。如果竞争锁失败,说明当前锁已经偏向了其他线程。
需要将锁升级到轻量级锁,在轻量级锁状态下,竞争锁的线程根据自适应自旋次数去尝试抢占锁资源,如果在轻量级锁状态下还是没有竞争到锁,
就只能升级到重量级锁,在重量级锁状态下,没有竞争到锁的线程就会被阻塞,线程状态是Blocked。
各种锁优缺点、synchronized锁升级和实现原理
偏向锁: 适用于单线程适用的情况,在不存在锁竞争的时候进入同步方法/代码块则使用偏向锁。
轻量级锁: 适用于竞争较不激烈的情况(这和乐观锁的使用范围类似), 存在竞争时升级为轻量级锁,轻量级锁采用的是自旋锁,如果同步方法/代码块执行时间很短的话,采用轻量级锁虽然会占用cpu资源但是相对比使用重量级锁还是更高效。
重量级锁: 适用于竞争激烈的情况,如果同步方法/代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就需要升级为重量级锁。
17.其他锁还知道哪些
CAS(乐观锁思想)
ReetrantLock(可重入锁)
18.线程的创建方法
4种
- Thread
- Runnable
- Callable
- 线程池
19.线程的生命周期
初始化状态(NEW)、可运行/运行状态(RUNNABLE)、阻塞状态(BLOCKED)、无时限等待状态(WAITING)、有时限等待状态(TIMED_WAITING)、终止状态(TERMINATED)
20.sleep和wait的区别
如果不熟悉可以看这篇
java多线程中sleep和wait的区别
1、sleep是线程中的方法,但是wait是Object中的方法。
2、sleep方法不会释放资源锁,但是wait会释放资源锁,而且会加入到等待队列中。
3、sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
4、sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。
21.notify和notifyAll的区别
notify和notifyAll一般和wait连用(适用于线程通信)
wait(): 一旦执行此方法,当前线程就进入阻塞状态,并且释放同步监视器
notify(): 一旦执行此方法,就会唤醒wait的一个线程,如果右多个线程被wait,就唤醒优先级高的线程
**notifyAll():**一旦执行此方法,就会唤醒所有被wait的线程
22.ArrayList和LinkedList的区别,他们的查询效率谁快,为什么
ArrayList
- 基于数组,需要连续内存
- 随机访问快(指根据下标访问)
- 尾部插入、删除性能可以,其它部分插入、删除都会移动数据,因此性能会低
- 可以利用 cpu 缓存,局部性原理
LinkedList
- 基于双向链表,无需连续内存
- 随机访问慢(要沿着链表遍历)
- 头尾插入删除性能高
- 占用内存多
23.HashMap的底层原理
在 JDK 1.7 中,HashMap 使用的是数组 + 链表实现。
在 JDK 1.8 中使用的是数组 + 链表或红黑树实现的。
put实现原理
(1)先判断当前Node[]数组是不是为空,为空就新建,不为空就对hash值与容量-1做与运算得到数组下标
(2)然后会判断当前数组位置有没有元素,没有的话就把值插到当前位置,有的话就说明遇到了哈希碰撞
(3)遇到哈希碰撞后,如果Hash值相同且equals内容也相同,直接覆盖,就会看下当前链表是不是以红黑树的方式存储,是的话,就会遍历红黑树,看有没有相同key的元素,有就覆盖,没有就执行红黑树插入
(4)如果是普通链表,则按普通链表的方式遍历链表的元素,判断p.next = null的情况下,直接存放追加在next后面,然后我们要检查一下如果链表长度大于8且数组容量>=64链表转换成红黑树,否则查找到链表中是否存在该key,如果存在直接修改value值,如果没有继续遍历
(5)如果++size > threshold(阈值)就扩容
HashMap中put的实现原理
HashMap何时会链表转红黑树
24.HashMap多线程的时候会出现什么情况,要怎么解决
ConcurrentHashMap
JDK1.7ConcurrentHashMap底层实现原理:
数据结构:Segment(大数组) + HashEntry(小数组) + 链表,每个 Segment 对应一把锁,如果多个线程访问不同的 Segment,则不会冲突
JDK1.8ConcurrentHashMap底层实现原理:
数据结构:Node 数组 + 链表或红黑树,数组的每个头节点作为锁,如果多个线程访问的头节点不同,则不会冲突。首次生成头节点时如果发生竞争,利用 cas 而非 syncronized,进一步提升性能
ConcurrentHashMap的整体架构
数据结构:Node 数组 + 链表或红黑树,数组的每个头节点作为锁,如果多个线程访问的头节点不同,则不会冲突。首次生成头节点时如果发生竞争,利用 cas 而非 syncronized,进一步提升性能
这个是ConcurrentHashMap在JDK1.8中的存储结构,它是由数组、单向链表、红黑树组成。
当我们初始化一个ConcurrentHashMap实例时,默认会初始化一个长度为16的数组。由于ConcurrentHashMap它的核心仍然是hash表,所以必然会存在hash冲突问题。
ConcurrentHashMap采用链式寻址法来解决hash冲突。
当hash冲突比较多的时候,会造成链表长度较长,这种情况会使得ConcurrentHashMap中数据元素的查询复杂度变成O(n)。因此在JDK1.8中,引入了红黑树的机制。
当数组长度大于64并且链表长度大于等于8的时候,单项链表就会转换为红黑树。
另外,随着ConcurrentHashMap的动态扩容,一旦链表长度小于8,红黑树会退化成单向链表。
ConcurrentHashMap的基本功能
ConcurrentHashMap本质上是一个HashMap,因此功能和HashMap一样,但是ConcurrentHashMap在HashMap的基础上,提供了并发安全的实现。
并发安全的主要实现是通过对指定的Node节点加锁,来保证数据更新的安全性。
ConcurrentHashMap在性能方面做的优化
- 在JDK1.8中,ConcurrentHashMap锁的粒度是数组中的某一个节点,而在JDK1.7,锁定的是Segment,锁的范围要更大,因此性能上会更低。
- 引入红黑树,降低了数据查询的时间复杂度,红黑树的时间复杂度是O(logn)。
- 当数组长度不够时,ConcurrentHashMap需要对数组进行扩容,在扩容的实现上,ConcurrentHashMap引入了多线程并发扩容的机制,简单来说就是多个线程对原始数组进行分片后,每个线程负责一个分片的数据迁移,从而提升了扩容过程中数据迁移的效率。
- ConcurrentHashMap中有一个size()方法来获取总的元素个数,而在多线程并发场景中,在保证原子性的前提下来实现元素个数的累加,性能是非常低的。ConcurrentHashMap在这个方面的优化主要体现在两个点:
- 当线程竞争不激烈时,直接采用CAS来实现元素个数的原子递增。
如果线程竞争激烈,使用一个数组来维护元素个数,如果要增加总的元素个数,则直接从数组中随机选择一个,再通过CAS实现原子递增。它的核心思想是引入了数组来实现对并发更新的负载。
为什么不用ReentrantLock而用synchronized ?
减少内存开销:如果使用ReentrantLock则需要节点继承AQS来获得同步支持,增加内存开销,而1.8中只有头节点需要进行同步。
内部优化:synchronized则是JVM直接支持的,JVM能够在运行时作出相应的优化措施:锁粗化、锁消除、锁自旋等等。
25.List线程安全的子类了解多少种
Vector和SynchronizedList
SynchronizedList
它能把所有 List 接口的实现类转换成线程安全的List,比 Vector 有更好的扩展性和兼容性
SynchronizedList的部分方法源码如下:
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
synchronized (mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
synchronized (mutex) {return list.remove(index);}
}
它所有方法都是带同步对象锁的,和 Vector 一样,不是性能最优的
面试官可能还会继续往下追问,比如在读多写少的情况,SynchronizedList这种集合性能非常差,还有没有更合适的方案?
并发包里面的并发集合类:java.util.concurrent.CopyOnWriteArrayList
CopyOnWriteArrayList:即复制再写入,就是在添加元素的时候,先把原 List 列表复制一份,再添加新的元素。
先来看下它的 add 方法源码:
public boolean add(E e) {
// 加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 获取原始集合
Object[] elements = getArray();
int len = elements.length;
// 复制一个新集合
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
// 替换原始集合为新集合
setArray(newElements);
return true;
} finally {
// 释放锁
lock.unlock();
}
}
添加元素时,先加锁,再进行复制替换操作,最后再释放锁。
再来看下它的 get 方法源码:
private E get(Object[] a, int index) {
return (E) a[index];
}
public E get(int index) {
return get(getArray(), index);
}
可以看到,获取元素并没有加锁。
这样做的好处是,在高并发情况下,读取元素时就不用加锁,写数据时才加锁,大大提升了读取性能。
26.JVM的内存结构
27.栈和堆他们存储速度上谁快?
栈快,栈管运行,堆管存储
栈和堆的区别?
-
角度一:GC;OOM
栈不存在GC,存在OMM
堆存在GC,存在OMM -
角度二:栈、堆执行效率
栈效率高 -
角度三:内存大小;数据结构
栈:java5.0之前,默认大小:256k 5.0之后,默认大小:1024k
堆:最大值为物理内存的1/4。对于64位虚拟机,如果物理内存为128G,那么heap最多可以达到32G。 -
角度四:栈管运行;堆管存储。
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
28.说下GC,常见的回收算法
如果不熟悉可以看这篇
GC回收算法
- 标记-清除算法
执行过程:
标记: Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
清除: Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。
缺点
1、效率比较低:递归与全堆对象遍历两次
2、在进行GC的时候,需要停止整个应用程序,导致用户体验差
3、这种方式清理出来的空闲内存是不连续的,产生内存碎片。
- 复制算法
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
优点:
没有标记和清除过程,实现简单,运行高效
复制过去以后保证空间的连续性,不会出现“碎片”问题。
缺点:
此算法的缺点也是很明显的,就是需要两倍的内存空间。
对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小。
应用场景
在新生代,对常规应用的垃圾回收,一次通常可以回收70%-99%的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代。
比如:IBM 公司的专门研究表明,新生代中 80% 的对象都是“朝生夕死”的。
- 标记-压缩算法
执行过程:
第一阶段和标记-清除算法一样,从根节点开始标记所有被引用对象
第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。
之后, 清理边界外所有的空间。
标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark-Sweep-Compact)算法。
**年轻代特点:**区域相对老年代较小,对象生命周期短、存活率低,回收频繁。
这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。
老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。
这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记-清除或者是标记-清除与标记-整理的混合实现。
29.堆的结构
30.类加载机制
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。
都谁需要加载?
在Java中数据类型分为基本数据类型和引用数据类型。基本数据类型由虚拟机预先定义,引用数据类型则需要进行类的加载。
基本类型包括:byte,short,int,long,char,float,double,Boolean,returnAddress,
引用类型包括:类类型,接口类型和数组。
类的生命周期
整个生命周期包括:
- Loading(装载)阶段
- Linking(链接)阶段
验证、准备、解析 - Initialization(初始化)阶段
- 类的Using(使用)
- 类的Unloading(卸载)
31.双亲委派是什么,为什么这么设计?
定义: 如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回。只有父类加载器无法完成此加载任务时,才自己去加载。
本质
规定了类加载的顺序是:引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由系统类加载器或自定义的类加载器进行加载。
优点
- 避免类的重复加载, 确保一个类的全局唯一性(防止重复加载同一个.class)
Java 类随着它的类加载器一起具备了一种带有优先级的层级关系, 通过这种层级关系可以避免类的重复加载, 当父亲已经加载了该类时, 就没有必要子ClassLoader 再加载一次 - 保护程序安全, 防止核心 API 被随意篡改
因为Bootstrap ClassLoader在加载的时候,只会加载JAVA_HOME中的jar包里面的类,如java.lang.Integer,那么这个类是不会被随意替换的,除非有人跑到你的机器上, 破坏你的JDK。
缺点
- 检查类是否加载的委托过程是单向的,这个方式虽然从结构上说比较清晰,使各个ClassLoader的职责非常明确,但是同时会带来一个问题,即项层的ClassLoader无法访问底层的ClassLoader所加载的类。
32.说下OSI七层模型
应用层: 网络服务与最终用户的一个接口,常见的协议有:HTTP FTP SMTP SNMP DNS.
表示层: 数据的表示、安全、压缩。,确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。
会话层: 建立、管理、终止会话,对应主机进程,指本地主机与远程主机正在进行的会话.
传输层: 定义传输数据的协议端口号,以及流控和差错校验,协议有TCP UDP.
网络层: 进行逻辑地址寻址,实现不同网络之间的路径选择,协议有ICMP IGMP IP等.
数据链路层: 在物理层提供比特流服务的基础上,建立相邻结点之间的数据链路。
物理层: 建立、维护、断开物理连接。
33.HTTP和HTTPS的区别
思路: 这道题实际上考察的知识点是HTTP与HTTPS的区别,这个知识点非常重要,可以从安全性、数据是否加密、默认端口等这几个方面去回答哈。其实,当你理解HTTPS的整个流程,就可以很好回答这个问题啦。
HTTP
- 即超文本传输协议,是一个基于TCP/IP通信协议来传递明文数据的协议。HTTP会存在这几个问题:
- 请求信息是明文传输,容易被窃听截取。
- 没有验证对方身份,存在被冒充的风险
- 数据的完整性未校验,容易被中间人篡改
HTTPS
- HTTPS= HTTP+SSL/TLS,可以理解Https是身披SSL(Secure Socket Layer,安全套接层)的HTTP,也就是用SSL/TLS对数据进行加密和解密,Http进行传输。。
34.TLS的握手过程
SSL / TLS 握手详细过程
"client hello"消息: 客户端通过发送"client hello"消息向服务器发起握手请求,该消息包含了客户端所支持的 TLS 版本和密码组合以供服务器进行选择,还有一个"client random"随机字符串。
"server hello"消息: 服务器发送"server hello"消息对客户端进行回应,该消息包含了数字证书,服务器选择的密码组合和"server random"随机字符串。
**验证:**客户端对服务器发来的证书进行验证,确保对方的合法身份,验证过程可以细化为以下几个步骤:
- 检查数字签名
- 验证证书链 (这个概念下面会进行说明)
- 检查证书的有效期
- 检查证书的撤回状态 (撤回代表证书已失效)
"premaster secret"字符串: 客户端向服务器发送另一个随机字符串"premaster secret (预主密钥)“,这个字符串是经过服务器的公钥加密过的,只有对应的私钥才能解密。
使用私钥: 服务器使用私钥解密"premaster secret”。
生成共享密钥:客户端和服务器均使用 client random,server random 和 premaster secret,并通过相同的算法生成相同的共享密钥 KEY。
客户端就绪: 客户端发送经过共享密钥 KEY加密过的"finished"信号。
服务器就绪: 服务器发送经过共享密钥 KEY加密过的"finished"信号。
达成安全通信: 握手完成,双方使用对称加密进行安全通信。
35.对称加密和非对称加密的区别
对称加密: 指加密和解密使用同一密钥
- 优点:是运算速度较快,缺点是如何安全将密钥传输给另一方。常见的对称加密算法有:DES、AES等。
- 缺点:如果第三方知道了加密的规则以后,就很容易被破解
非对称加密
指的是加密和解密使用不同的密钥(即公钥和私钥)。公钥与私钥是成对存在的,如果用公钥对数据进行加密,只有对应的私钥才能解密。
- 优点:算法公开,加密和解密使用不同的钥匙,私钥不需要通过网络进行传输,安全性很高。
- 缺点:计算量比较大,加密和解密速度相比对称加密慢很多。
36.mybatis的#和$区别
- 【#】底层执行SQL语句的对象,使用PreparedStatementd,预编译SQL,防止SQL注入安全隐患,相对比较安全。
- 【 $ 】底层执行SQL语句的对象使用Statement对象,未解决SQL注入安全隐患,相对不安全。
Mybatis提供到的#号占位符和$号占位符,都是实现动态SQL的一种方式,通过这两种方式把参数传递到XML之后,在执行操作之前,Mybatis会对这两种占位符进行动态解析。
#号占位符,等同于jdbc里面的?号占位符。
它相当于向PreparedStatement中的预处理语句中设置参数,
而PreparedStatement中的sql语句是预编译的,SQL语句中使用了占位符,规定了sql语句的结构。
并且在设置参数的时候,如果有特殊字符,会自动进行转义。
所以#号占位符可以防止SQL注入。
而使用$的方式传参,相当于直接把参数拼接到了原始的SQL里面,Mybatis不会对它进行特殊处理。
所以 $ 和#最大的区别在于,前者是动态参数,后者是占位符, 动态参数无法防止SQL注入的问题,所以在实际应用中,应该尽可能的使用#号占位符。
另外,$符号的动态传参,可以适合应用在一些动态SQL场景中,比如动态传递表名、动态设置排序字段等。
#与$使用场景
查询SQL:select col,col2 from table1 where col=? and col2=? group by ?, order by ? limit ?,?
- #使用场景,sql占位符位置均可以使用#
- $ 使用场景,#解决不了的参数传递问题,均可以交给$处理【如:form 动态化表名】
/**
* 测试$使用场景
*/
public List<Employee> selectEmpByDynamitTable(@Param("tblName") String tblName);
<select id="selectEmpByDynamitTable" resultType="employee">
SELECT
id,
last_name,
email,
salary
FROM
${tblName}
</select>
彻底理解SQL注入
最简单的例子:一般开发,肯定是在前台有两个输入框,一个用户名,一个密码,会在后台里,读取前台传入的这两个参数,拼成一段SQL,例如: select count(1) from tab where usesr=userinput and pass = passinput,把这段SQL连接数据后,看这个用户名/密码是否存在,如果存在的话,就可以登陆成功了,如果不存在,就报一个登陆失败的错误。对吧。
但是有这样的情况,这段SQL是根据用户输入拼出来,如果用户故意输入可以让后台解析失败的字符串,这就是SQL注入,例如,用户在输入密码的时候,输入 ‘’‘’ ’ or 1=1’‘, 这样,后台的程序在解析的时候,拼成的SQL语句,可能是这样的: select count(1) from tab where user=userinput and pass=’’ or 1=1; 看这条语句,可以知道,在解析之后,用户没有输入密码,加了一个恒等的条件 1=1,这样,这段SQL执行的时候,返回的 count值肯定大于1的,如果程序的逻辑没加过多的判断,这样就能够使用用户名 userinput登陆,而不需要密码。
防止SQL注入,首先要对密码输入中的单引号进行过滤,再在后面加其它的逻辑判断,或者不用这样的动态SQL拼接
37.mybatis的二级缓存是什么,分别的作用范围
Mybatis缓存机制之一级缓存
-
概述:一级缓存【本地缓存(Local Cache)或SqlSession级别缓存】
-
特点
- 一级缓存默认开启
- 不能关闭
- 可以清空
-
缓存原理
- 第一次获取数据时,先从数据库中加载数据,将数据缓存至Mybatis一级缓存中【缓存底层实现原理Map,key:hashCode+查询的SqlId+编写的sql查询语句+参数】
- 以后再次获取数据时,先从一级缓存中获取,如未获取到数据,再从数据库中获取数据。
一级缓存五种失效情况
-
不同的SqlSession对应不同的一级缓存
-
同一个SqlSession但是查询条件不同
-
同一个SqlSession两次查询期间执行了任何一次增删改操作
清空一级缓存 -
同一个SqlSession两次查询期间手动清空了缓存
sqlSession.clearCache() -
同一个SqlSession两次查询期间提交了事务
sqlSession.commit()
Mybatis缓存机制之二级缓存
-
二级缓存【second level cache】概述
- 二级缓存【全局作用域缓存】
- mapper级别的缓存
-
二级缓存特点
- 二级缓存默认关闭,需要开启才能使用
- 二级缓存需要提交sqlSession或关闭sqlSession时,才会缓存。
-
二级缓存的失效情况
- 在两次查询之间,执行增删改操作,会同时清空一级缓存和二级缓存
- sqlSession.clearCache():只是用来清除一级缓存。
MyBatis缓存查询的顺序
- 先查询二级缓存,因为二级缓存中可能会有其他程序已经查出来的数据,可以拿来直接使用。
- 如果二级缓存没有命中,再查询—级缓存
- 如果一级缓存也没有命中,则查询数据库
- SqlSession关闭之后,一级缓存中的数据会写入二级缓存
38.Spring是什么
Spring是一个轻量级的IOC和AOP容器框架。是为Java应用程序提供基础性服务的一套框架,目的是用于简化企业应用程序的开发,它使得开发者只需要关心业务需求。
39.动态代理有哪两种,分别怎么实现的知道吗
JDK动态代理:
JDK动态代理是Java官方提供的代理实现方式,主要依赖于java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口。JDK动态代理要求目标对象必须实现一个或多个接口。
实现步骤:
- 创建一个实现InvocationHandler接口的类,重写invoke方法。这个方法将在代理对象调用目标方法时被执行,包含代理逻辑。
- 使用Proxy类的newProxyInstance方法创建代理对象。这个方法接收三个参数:目标对象的类加载器、目标对象实现的接口列表和InvocationHandler实现类的实例。
- 调用代理对象的方法。当代理对象的方法被调用时,InvocationHandler的invoke方法会被执行,插入代理逻辑。
优点: JDK动态代理是官方提供的代理实现,无需引入额外的依赖
缺点: JDK动态代理要求目标对象必须实现一个或多个接口。如果目标对象没有实现接口,无法使用JDK动态代理。此外,JDK动态代理只能为接口创建代理对象,不能针对类进行代理。
CGLIB代理:
CGLIB(Code Generation Library)是一个第三方库,可以在运行时动态生成代理对象。CGLIB代理基于继承机制,要求目标对象不能是final类。
实现步骤:
- 创建一个实现MethodInterceptor接口的类,重写intercept方法。这个方法将在代理对象调用目标方法时被执行,包含代理逻辑。
- 使用CGLIB的Enhancer类创建代理对象。需要设置目标对象的类、回调方法(MethodInterceptor实现类的实例)等属性。
- 调用代理对象的方法。当代理对象的方法被调用时,MethodInterceptor的intercept方法会被执行,插入代理逻辑。
优点: CGLIB代理可以针对没有实现接口的类进行代理,功能更加强大。
缺点: CGLIB代理需要引入额外的依赖(cglib.jar),而且性能相对较低。另外,CGLIB代理不能针对final类进行代理,因为它使用继承机制实现代理。
总结:
动态代理在Java中主要有两种实现方式:JDK动态代理和CGLIB代理。JDK动态代理是官方提供的代理实现,性能较好,但要求目标对象实现接口。CGLIB代理是第三方库实现的代理,可以针对没有实现接口的类进行代理,功能更强大,但性能较低。在选择动态代理实现时,需要根据具体场景和需求进行权衡。