Java高频面试题
2023-04-26 18:11:47 20 举报
Java高频面试题
作者其他创作
大纲/内容
集合
Collection 和 Collections 区别
Collection
是一个集合类的顶级接口
其直接继承接口有 List 与 Set
为各种具体的集合提供了最大化的统一操作方式
Collections
集合类的一个工具类/帮助类
提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。
ArrayList 和 LinkList区别
两者都不是线程安全
底层数据结构不同
Arraylist——Object 数组
LinkedList——双向链表
效率不同
随机访问、修改时
ArrayList效率更高,
因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找。
因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找。
插入、删除时
LinkedList效率更高,
因为ArrayList是数组,在进行增删操作时,
会对操作点之后所有数据的下标索引造成影响,需要进行数据的移动。
因为ArrayList是数组,在进行增删操作时,
会对操作点之后所有数据的下标索引造成影响,需要进行数据的移动。
总结:数组修查快增删慢,链表增删快修查慢
扩容不同
jdk8的ArrayList,在第一次调用 add 方法就会创建容量为 10 的数组,无特殊情况扩容一般是 1.5 倍
Linkedlist 不会扩容,链表一直加
主要控件开销不同
ArrayList主要控件开销在于需要在List列表预留一定空间
LinkList主要控件开销在于需要存储节点信息以及节点指针
HashSet、LinkedHashSet 和 TreeSet
HashSet 是 Set 接口的主要实现类,底层是 HashMap,线程不安全,可存储 null
LinkedHashSet 是 HashSet 的子类,能够按照添加的顺序遍历;
TreeSet 底层使用红黑树,元素是有序的,排序的方式有自然排序和定制排序。
HashMap、HashTable、TreeMap区别
HashMap:无序、非线程安全、效率较高、允许null值(key和value都允许)
HashTable:无序、线程安全、效率较低、不允许key值为null
TreeMap:有序(基于红黑树对所有的key进行排序)、非线程安全、没有调优选项(因为该树总处于平衡状态)、不允许key值为null
HashMap 和 ConcurrentHashMap 的区别
HashMap:key可为null
ConCurrentHashMap:key不可为空
ConCurrentHashMap:key不可为空
线程安全
HashMap
没有锁机制,不是线程安全
ConcurrentHashMap
对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用 lock 锁进行保护。
相对于 HashTable 的 synchronized 锁的粒度更精细了一些,并发性能更好。
JDK1.8 之后ConcurrentHashMap 启用了一种全新的方式实现,利用 CAS 算法
HashMap
基本特性
线程不安全
允许键为空(一个)
元素是无序的,且顺序会不定时改变
底层数据结构
JDK1.7,由“数组+链表”组成,数组是主体,链表则是为了解决哈希冲突而存在的
JDK1.8,由“数组+链表+红黑树”组成。当链表过长,会影响性能,链表搜索复杂度是O(n),红黑树是O(logn)。
链表和红黑树的转换
当链表长度>=8,且数组长度>=64,才会转红黑树
当链表长度<=6时,才会转化为链表(去树化),中间有个差值7,防止频繁转换
为什么阈值为8,根据统计学的泊松分布
加载因子/扩容
默认值:加载因子0.75,初始长度为16,阈值为12(16*0.75)
扩容条件
当存新值时(不是替换),map的大小如果大于阈值,触发扩容
注意,是先存放后扩容
扩容位原来的2倍
PUT流程
根据key计算hash值,找到该元素在数组中存储的下标
如果数组为空,调用resize初始化
如果没有哈希冲突,直接放在对应的数组下标里
如果冲突,则判断key是否存在,存在则覆盖掉value
如果key不存在,且是红黑树,则插入键值
如果key不存在,且是链表,则插入键值,再判断如果链表长度>=8(此处长度不包含新节点)
数组长度<64,进行扩容
数组长度>=64,转红黑树
最后判断是否大于阈值,是则扩容
其他
remove()时,左右节点为空,去树化,转为链表
在resize()时也会发生去树化
在put时,如果key存在,会返回旧值,否则返回null
当key为null时,存在数组下标为0里面
1.7和1.8插入时的区别
7 只使用链表,8 使用链表+红黑树
7是先扩容再存放,8是先存放后扩容
7 使用头插法插入元素,在多线程的环境下有可能导致环形链表的出现,扩容的时候会导致死循环。
因此,8使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了,
但JDK1.8 的 HashMap 仍然是线程不安全的
因此,8使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了,
但JDK1.8 的 HashMap 仍然是线程不安全的
key 的存储索引是怎么计算?
int h = key.hashCode();
h ^ (h >>> 16)
h ^ (h >>> 16)
hashCode 异或 无符号右移16位的hashCode
public native int hashCode();
初始化大小
HashMap 构造函数允许用户传入的容量不是2的n次方,
因为它可以自动地将传入的容量转换为2的n 次方。
会取大于或等于这个数的 且最近的2次幂作为 table 数组的初始容量,
使用tableSizeFor(int)方法,
如 tableSizeFor(10) = 16(2 的 4 次幂),tableSizeFor(20) = 32(2 的 5 次幂),
也就是说 table 数组的长度总是 2 的次幂
因为它可以自动地将传入的容量转换为2的n 次方。
会取大于或等于这个数的 且最近的2次幂作为 table 数组的初始容量,
使用tableSizeFor(int)方法,
如 tableSizeFor(10) = 16(2 的 4 次幂),tableSizeFor(20) = 32(2 的 5 次幂),
也就是说 table 数组的长度总是 2 的次幂
modCount
modCount用于记录HashMap的修改次数,在HashMap的put(),get(),remove(),Interator()等方法中,都使用了该属性。
由于HashMap不是线程安全的,所以在迭代的时候,会将modCount赋值到迭代器的expectedModCount属性中,然后进行迭代;
如果在迭代的过程中HashMap被其他线程修改了,modCount的数值就会发生变化,这个时候expectedModCount和ModCount不相等,迭代器就会抛出ConcurrentModificationException()并发修改异常。
由于HashMap不是线程安全的,所以在迭代的时候,会将modCount赋值到迭代器的expectedModCount属性中,然后进行迭代;
如果在迭代的过程中HashMap被其他线程修改了,modCount的数值就会发生变化,这个时候expectedModCount和ModCount不相等,迭代器就会抛出ConcurrentModificationException()并发修改异常。
推荐网站
https://www.pudn.com/news/62a731ef3934cd25af8301a3.html
锁
锁的分类
自旋锁
是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,
那么该线程将循环等待并尝试获取,不断的判断锁是否能够被成功获取,直到成功获取到锁才会退出循环。
那么该线程将循环等待并尝试获取,不断的判断锁是否能够被成功获取,直到成功获取到锁才会退出循环。
分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁
分段锁设计的目的是细化锁的粒度,当操作不需要更新整个对象时,就仅仅针对对象中的部分进行加锁操作。
如ConcurrentHashMap的实现
从线程是否需要对资源加锁
乐观锁/悲观锁
说明
并不是具体的两种锁的实现,而一种设计思想,一种看待并发同步的角度
悲观锁
说明
对于同一数据的并发操作,悲观锁认为这是一定会修改数据的,因此会采取加锁的方式实现同步。
由于数据进行加锁,期间对该数据进行读写的其他线程都会进行等待
也就是说,持有数据就会上锁
案例
select * from user where name="sun" for update
这条sql语句从user 表中选取name=sun的记录并对其加锁,
那么其他写操作再这个事务提交之前,都不会对这条数据进行操作,起到了独占和排他的作用。
实现
Synchronized
Java中的Synchronized就是悲观锁
ReentrantLock等独占锁(互斥锁)
ReentrantLock底层由AQS实现:
先尝试CAS乐观锁去获取锁,获取不到,才会转换为悲观锁
先尝试CAS乐观锁去获取锁,获取不到,才会转换为悲观锁
优/缺点
优点
保证多线程下顺序读写,防止脏数据产生
缺点
浪费CPU、执行效率低
占用资源,使其他线程无法工作
适用场景
适用读少写多,该场景下,冲突多,所以悲观锁很适合
传统关系型数据库里,有很多这种锁机制,比如行锁,表锁等,都是在做操作之前先上锁
比如MySQL的for update
乐观锁
说明
认为读多写少,遇到并发的可能性低,所以不会上锁
读时不加锁,更新时通过业务实现锁
两种实现方案
CAS算法
什么是CAS算法?
CAS(Compare-and-Swap, 即比较并替换) 算法
一种有名的无锁算法
不使用锁的情况下,实现多线程之间的变量同步
Java中的乐观锁基本都是通过CAS操作实现的
Java从JDK 1.5开始支持
一些以Atomic为开头的一些原子类都使用CAS作为其实现方式
使用这些类在多核CPU的机器上会有比较好的性能。
如何保证原子性?
保证原子性,需加锁,如synchronzied或ReentrantLock
涉及三要素
V:需要读写的内存值
A:进行比较的值
B:拟写入的新值
只有A和V内存值相同,才将V修改为B,否则什么都不做
优/缺点
优点
在没有线程被阻塞的情况下,实现变量的同步
非阻塞的轻量级的乐观锁,在资源竞争不激烈的情况下性能高,
相比synchronized重量锁,synchronized会进行比较复杂的加锁、解锁和唤醒操作。
相比synchronized重量锁,synchronized会进行比较复杂的加锁、解锁和唤醒操作。
缺点
ABA问题
一开始值为A,但被其他线程修改过,但值被修改成A
自旋时间过长,消耗CPU资源,
如果资源竞争激烈,多线程自旋长时间消耗资源
如果资源竞争激烈,多线程自旋长时间消耗资源
在高并发的情况下,性能可能还不如悲观锁
版本号机制
也叫DCAS,是CAS的变种,加了个版本号标识
在数据表中加上一个version字段来实现,
表示数据被修改的次数, 当执行写操作并且写入成功后, version=version+1,当线程A要更新数据时,
在读取数据的同时也会读取version值,
在提交更新时, 若刚才读取到的version值为当前数据库中的version值相等时,才更新, 否则重试更新操作,直到更新成功。
表示数据被修改的次数, 当执行写操作并且写入成功后, version=version+1,当线程A要更新数据时,
在读取数据的同时也会读取version值,
在提交更新时, 若刚才读取到的version值为当前数据库中的version值相等时,才更新, 否则重试更新操作,直到更新成功。
缺点
ABA问题
循环开销大
乐观锁在进行写操作时,会判断是否能够写入成功,如果写入不成功将触发等待→重试机制,
这种情况是一个自旋锁,简单来说就是适用于短期内获取不到,进行等待重试的锁,
它不适用于长期获取不到锁的情况,
另外,自旋循环对于性能开销比较大。
这种情况是一个自旋锁,简单来说就是适用于短期内获取不到,进行等待重试的锁,
它不适用于长期获取不到锁的情况,
另外,自旋循环对于性能开销比较大。
CAS与synchronized的使用情景
CAS,适用于写比较少的情况下(多读场景, 冲突一般较少)
synchronized,适用于写比较多的情况下(多写场景,冲突一般较多)
从多线程并发访问资源(Synchronized实现细节)
偏向锁/轻量级锁/重量级锁
说明
这三种锁是指锁的状态,并且针对synchronized
在Java 5通过引入锁升级的机制来实现高效synchronized。
这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁
是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。
降低获取锁的代价
轻量级锁
当偏向锁的被另一个线程所访问,偏向锁就会升级为轻量级锁
其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能
重量级锁
指当锁为轻量级锁的时,另一个线程虽然是自旋,但自旋不会一直持续下去,
当自旋一定次数的时候,还没有获取到锁,
就会进入阻塞,该锁膨胀为重量级锁。
指当锁为轻量级锁的时,另一个线程虽然是自旋,但自旋不会一直持续下去,
当自旋一定次数的时候,还没有获取到锁,
就会进入阻塞,该锁膨胀为重量级锁。
重量级锁会让其他申请的线程进入阻塞,性能降低。
锁状态的切换
synchronized 关键字之锁的升级(偏向锁->轻量级锁->重量级锁)
Java 1.6 为了改善性能, 使得 JVM 会根据竞争情况, 使用了这三种锁机制
这三种机制是根据竞争激烈程度进行的,
在几乎无竞争的条件下, 会使用偏向锁,
在轻度竞争的条件下, 会由偏向锁升级为轻量级锁,
在重度竞争的情况下, 会升级到重量级锁。
在几乎无竞争的条件下, 会使用偏向锁,
在轻度竞争的条件下, 会由偏向锁升级为轻量级锁,
在重度竞争的情况下, 会升级到重量级锁。
无法逆向
公平锁/非公平锁
独享锁/共享锁
互斥锁/读写锁
可重入锁
MySQL
事务
默认事务是可重复读
类型
特性
原子性:指处于同一个事务中的多条语句是不可分割的。
一致性:事务必须使数据库从一个一致性状态变换到另外一个一致性状态。
比如转账,转账前两个账户余额之和为2k,转账之后也应该是2K。
比如转账,转账前两个账户余额之和为2k,转账之后也应该是2K。
隔离性:指多线程环境下,一个线程中的事务不能被其他线程中的事务打扰
持久性:事务一旦提交,就应该被永久保存起来。
隔离性问题
脏读:指一个线程中的事务读取到了另外一个线程中未提交的数据。
不可重复读:指一个线程中的事务读取到了另外一个线程中提交的update的数据。
幻读:指一个线程中的事务读取到了另外一个线程中提交的insert的数据。
优化
SQL优化
Explain
详解:
https://segmentfault.com/a/1190000039152427?utm_source=tag-newest
https://segmentfault.com/a/1190000039152427?utm_source=tag-newest
id
select_type
table
type
system
当表中只有一条记录并且该表使用的存储引擎的统计数据是精确的,
比如 MyISAM、Memory,那么对该表的访问方法就是 system
比如 MyISAM、Memory,那么对该表的访问方法就是 system
InnoDB 表即使只有一行,也不是 system,而是 ALL
const
出现在主键和唯一索引,只有一条数据
只能是等值匹配(=),不能是in or < 之类的
eq_ref
联表的时候通过主键或唯一索引进行等值关联
ref
普通索引进行等值匹配
ref_or_null
explain SELECT * from system_user WHERE dept_id =1 or dept_id is null
dept_id可以是普通索引,也可以是唯一索引
当对普通索引进行等值匹配查询,
该索引列的值也可以是 NULL 值时,
那么对该表的访问方法就可能是 ref_or_null
该索引列的值也可以是 NULL 值时,
那么对该表的访问方法就可能是 ref_or_null
index_merge
unique_subquery
range
explain select * from t1 where a<50 and a>20;
explain select * from t1 where a in(1,2,3);
a是索引
如果使用索引获取某些范围区间的记录,那么就可能使用到 range 访问方法
index
查询所有的索引
explain select key_1 from t1
该联接类型与ALL相同,但index是只查索引。
通常比ALL快,因为索引文件通常比数据文件小。
都是读全表,但index是从索引中读取的,而ALL是从硬盘中读的
通常比ALL快,因为索引文件通常比数据文件小。
都是读全表,但index是从索引中读取的,而ALL是从硬盘中读的
All
全表查询,很慢,应该避免
possible_keys / key
possible_keys 表示可能用到的索引有哪些
key 表示实际用到的索引有哪些
key_len
key_len 列显示 MySQL 决定使用的键长度。
如果键是 NULL,则长度为 NULL。
使用的索引的长度。在不损失精确性的情况下,长度越短越好 。
ref
当使用索引列等值匹配的条件去执行查询时,
也就是在访问方法是 const、eq_ref、ref、ref_or_null、unique_subquery、index_subquery 其中之一时,
ref 列展示的就是与索引列作等值匹配的对象是啥。
也就是在访问方法是 const、eq_ref、ref、ref_or_null、unique_subquery、index_subquery 其中之一时,
ref 列展示的就是与索引列作等值匹配的对象是啥。
如果不是等值查询,则显示为 NULL
简单理解:使用哪个列或常数与key一起从表中选择行
rows
如果是全表查询,rows 列就代表预计需要扫描的行数;
如果使用索引来执行查询,rows 列就代表预计扫描的索引记录行数。
有可能是个精确值,也可能是个估算值
Extra
显示 MySQL 在查询过程中的一些详细信息
防止条件字段函数操作
如果对字段做了函数计算,就用不上索引了,这是MySQL的规定
对索引字段做函数操作,可能会破坏索引值的有序性,
因此优化器就决定放弃走树搜索功能
因此优化器就决定放弃走树搜索功能
防止隐式类型转换
select * from t where id = 1
如果id是字符类型的,1是数字类型的,
用explain会发现走了全表扫描,根本用不上索引
用explain会发现走了全表扫描,根本用不上索引
因为MySQL底层会对你的比较进行转换,
相当于加了 CAST( id AS signed int) 这样的一个函数,
函数会导致走不上索引。
相当于加了 CAST( id AS signed int) 这样的一个函数,
函数会导致走不上索引。
最左匹配原则
like 'aa%'
联合索引(a,b,c)相当于建立了索引:(a),(a,b),(a,b,c)
ac也不走索引
加索引
主键索引
一个表只能一个主键
不允许值重复或者值为空
普通索引
没有任何限制
唯一索引
列的值必须唯一,但允许有空值
IN包含的值不应过多,能用between就不要用in
不要用*号
尽量用union all代替union
避免在where子句中对字段进行null值判断
对于null的判断会导致引擎放弃使用索引而进行全表扫描
优化数据库表结构的设计
字段的数据类型
数据类型的长度
合理利用表的存储引擎
MyISAM不支持事务,表级锁,但是查询速度快
InnoDB支持事务,行锁
分表
将一张表的数据拆分成多张表
每张表的数量减少,查询速度相对会快一些
大事务
避免一次处理太多的数据
移除不必要在事务中的select操作
数据库参数配置优化
例如最大连接数默认为100,即使SQL语句优化的再好,
硬件设备配置再高,当请求超过100时都要再等待
硬件设备配置再高,当请求超过100时都要再等待
主从复制,读写分离
通过使用MySQL主从复制,
增删改操作走Master主服务器,查询走Slaver从服务器,
这样就减少了只有一台MySQL服务器的压力
增删改操作走Master主服务器,查询走Slaver从服务器,
这样就减少了只有一台MySQL服务器的压力
增加缓存层
减少数据库连接
通过使用缓存服务器如redis、elasticsearch等增加缓存,减少数据库的连接
升级服务器硬件
更快的磁盘IO设备
更强的CPU
更大的内存
更大的网卡流量(带宽)
问题
为什么要用 B+树,而不用其他树?
为什么不是一般二叉树?
如果二叉树特殊化为一个链表相当于全表扫描了,影响性能
为什么不是平衡二叉树?
每个节点只存一个键值和数据,
如果是B+树,可以存储更多的节点,降低树的高度
如果是B+树,可以存储更多的节点,降低树的高度
为什么不是B树?
B+树的数据存储在叶子节点,且数据是按照顺序排列,且有一条链表连着,
那范围查找,排序查找,分组查找以及去重查找变得异常简单
那范围查找,排序查找,分组查找以及去重查找变得异常简单
B+树非叶子节点不存数据,只存键值,而B树节点不仅存键值,也存数据。
Innodb中页的默认大小是16KB,如果不存数据,那么就会存更多的键值,
相应的树的阶数(节点的子节点树)就会更大,树就会更矮更胖,
这样一来,查找数据时,与磁盘的IO交互次数就会减少,效率就会提高
Innodb中页的默认大小是16KB,如果不存数据,那么就会存更多的键值,
相应的树的阶数(节点的子节点树)就会更大,树就会更矮更胖,
这样一来,查找数据时,与磁盘的IO交互次数就会减少,效率就会提高
MyIsam引擎和Innodb引擎的区别
Innodb支持事务、行锁、外键,但MyIsam不支持
Innodb的的索引和数据是一起存储,但MyIsam是分开存储的
线程
生命周期图
常用方法
sleep
Thread的静态方法,释放CPU(bolcked),不释放锁
yield
Thread的静态方法,释放CPU(runnanle),不释放锁
wait
Object类,释放CPU(等待blocked),释放锁,必须在synchronized语句块内使用,配合notify()和notifyAll()使用
join
Thread的普通方法,抢占CPU,父线程等待子线程结束,
会释放thread对象锁,但不会释放当前调用线程持有的对象锁(包括main线程),
底层是wait方法,在JVM层面通过notify_all来唤醒
会释放thread对象锁,但不会释放当前调用线程持有的对象锁(包括main线程),
底层是wait方法,在JVM层面通过notify_all来唤醒
线程池
ThreadPoolExecutor 7个参数
1.corePoolSize:线程池中的常驻核心线程数
2.maxinumPoolSize:线程池中能够容纳同时执行的最大线程数,此值必须大于等于一
3.keepAliveTime:多余的空闲线程的存活时间。
当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止。
4.unit:keepAliveTime的单位
5.workQueue:任务队列,被提交但是尚未被执行的任务。
6.threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可。
7.handler:拒绝策略,表示当队列满了并且工作线程-大于等于线程池的数量最大线程数(maxinumPoolSize)时如何来拒绝请求执行的runnable的策略。
2.maxinumPoolSize:线程池中能够容纳同时执行的最大线程数,此值必须大于等于一
3.keepAliveTime:多余的空闲线程的存活时间。
当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止。
4.unit:keepAliveTime的单位
5.workQueue:任务队列,被提交但是尚未被执行的任务。
6.threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可。
7.handler:拒绝策略,表示当队列满了并且工作线程-大于等于线程池的数量最大线程数(maxinumPoolSize)时如何来拒绝请求执行的runnable的策略。
工作原理图
问题
阻塞队列的作用?为什么是先添加队列而不是先创建最大线程?
设计如此
在创建新线程的时候,是要获取全局锁的,这个时候其他的就需要阻塞,影响了整体效率
在创建新线程的时候,是要获取全局锁的,这个时候其他的就需要阻塞,影响了整体效率
shutdown和shutdownNow区别
shutdown
ExecutorService不会立即关闭,但不再接收新任务,在这之前提交的任务都会被执行,所有线程执行完成才会关闭,
shutdownNow
清空队列。但不对正在执行的任务做任何保证,有可能它们都会停止,也有可能执行完成
0 条评论
下一页