1.hashmap,concurrenthashmap 演变过程
简单说明下(想到哪说到哪,具体需要大家搜索补充):
hashMap 是 数组+链表 ,要理解entry对象。 在hash冲撞的情况下,会出现链表情况,因为链表遍历是从头遍历,O(n),所以效率不太好,为了解决这个情况,在哦jdk1.8 后 引入和红黑树。当然也不是上来就是直接存储采用的是红黑树的,有个flag,就是你的链表长度好像是达到8之后就数据结构变成红黑树,当然 如果长度回到8也不是立刻就成链表,会造成资源浪费。好像是6,变成链表。重点:
数组长度得超过默认长度64,没超过先扩容,多会它的长度超过64以上,节点size大于8才转成红黑树的结构。
面试中的问题(能回想起来的)
Q0:HashMap是如何定位下标的?
A:先获取Key,然后对Key进行hash,获取一个hash值,然后用hash值对HashMap的容量进行取余(实际上不是真的取余,而是使用按位与操作,原因参考Q6),最后得到下标。
Q1:HashMap由什么组成?
A:数组+单链表,jdk1.8以后又加了红黑树,当链表节点个数超过8个(m默认值)以后,开始使用红黑树,使用红黑树一个综合取优的选择,相对于其他数据结构,红黑树的查询和插入效率都比较高。而当红黑树的节点个数小于6个(默认值)以后,又开始使用链表。这两个阈值为什么不相同呢?主要是为了防止出现节点个数频繁在一个相同的数值来回切换,举个极端例子,现在单链表的节点个数是9,开始变成红黑树,然后红黑树节点个数又变成8,就又得变成单链表,然后节点个数又变成9,就又得变成红黑树,这样的情况消耗严重浪费,因此干脆错开两个阈值的大小,使得变成红黑树后“不那么容易”就需要变回单链表,同样,使得变成单链表后,“不那么容易”就需要变回红黑树。
Q2:Java的HashMap为什么不用取余的方式存储数据?
A:实际上HashMap的indexFor方法用的是跟HashMap的容量-1做按位与操作,而不是%求余。(这里有个硬性要求,容量必须是2的指数倍,原因参考Q6)
Q3:HashMap往链表里插入节点的方式?
A:jdk1.7以前是头插法,jdk1.8以后是尾插法,因为引入红黑树之后,就需要判断单链表的节点个数(超过8个后要转换成红黑树),所以干脆使用尾插法,正好遍历单链表,读取节点个数。也正是因为尾插法,使得HashMap在插入节点时,可以判断是否有重复节点。
Q4:HashMap默认容量和负载因子的大小是多少?
A:jdk1.7以前默认容量是16,负载因子是0.75。
Q5:HashMap初始化时,如果指定容量大小为10,那么实际大小是多少?
A:16,因为HashMap的初始化函数中规定容量大小要是2的指数倍,即2,4,8,16,所以当指定容量为10时,实际容量为16。
A:两个原因:1,首先求余和位运算效果都是一样的,都不会超出集合长度提升计算效率:因为2的指数倍的二进制都是只有一个1,而2的指数倍-1的二进制就都是左全0右全1。那么跟(2^n - 1)做按位与运算的话,得到的值就一定在【0,(2^n - 1)】区间内,这样的数就刚合适可以用来作为哈希表的容量大小,因为往哈希表里插入数据,就是要对其容量大小取余,从而得到下标。所以用2^n做为容量大小的话,就可以用按位与操作替代取余操作,提升计算效率。2.便于动态扩容后的重新计算哈希位置时能均匀分布元素:因为动态扩容仍然是按照2的指数倍,所以按位与操作的值的变化就是二进制高位+1,比如16扩容到32,二进制变化就是从0000 1111(即15)到0001 1111(即31),那么这种变化就会使得需要扩容的元素的哈希值重新按位与操作之后所得的下标值要么不变,要么+16(即挪动扩容后容量的一半的位置),这样就能使得原本在同一个链表上的元素均匀(相隔扩容后的容量的一半)分布到新的哈希表中。(注意:原因2(也可以理解成优点2),在jdk1.8之后才被发现并使用)
Q7:HashMap满足扩容条件的大小(即扩容阈值)怎么计算?
A:扩容阈值=min(容量*负载因子,MAXIMUM_CAPACITY+1),MAXIMUM_CAPACITY非常大,所以一般都是取(容量*负载因子)
Q8:HashMap是否支持元素为null?
A:支持。
HashMap的 hash(Obeject k)方法中为什么在调用 k.hashCode()方法获得hash值后,为什么不直接对这个hash进行取余,而是还要将hash值进行右移和异或运算?
A:如果HashMap容量比较小而hash值比较大的时候,哈希冲突就容易变多。基于HashMap的indexFor底层设计,假设容量为16,那么就要对二进制0000 1111(即15)进行按位与操作,那么hash值的二进制的高28位无论是多少,都没意义,因为都会被0&,变成0。所以哈希冲突容易变多。那么hash(Obeject k)方法中在调用 k.hashCode()方法获得hash值后,进行的一步运算:h^=(h>>>20)^(h>>>12);有什么用呢?首先,h>>>20和h>>>12是将h的二进制中高位右移变成低位。其次异或运算是利用了特性:同0异1原则,尽可能的使得h>>>20和h>>>12在将来做取余(按位与操作方式)时都参与到运算中去。综上,简单来说,通过h^=(h>>>20)^(h>>>12);运算,可以使k.hashCode()方法获得的hash值的二进制中高位尽可能多地参与按位与操作,从而减少哈希冲突。
Q10:哈希值相同,对象一定相同吗?对象相同,哈希值一定相同吗?
A:不一定。一定。
Q11:HashMap的扩容与插入元素的顺序关系?
A:jdk1.7以前是先扩容再插入,jdk1.8以后是先插入再扩容。
Q12:HashMap扩容的原因?
A:提升HashMap的get、put等方法的效率,因为如果不扩容,链表就会越来越长,导致插入和查询效率都会变低。
Q13:jdk1.8引入红黑树后,如果单链表节点个数超过8个,是否一定会树化?
A:不一定,它会先去判断是否需要扩容(即判断当前节点个数是否大于扩容的阈值),如果满足扩容条件,直接扩容,不会树化,因为扩容不仅能增加容量,还能缩短单链表的节点数,一举两得。
hashMap 1.7 基于头插入 在扩容的时候会顺序翻转【在扩容的时候会有两个指针 head tail 主要是它两的指向错误】,导致多线程情况下出错。 如果基于尾部添加则不会,主要就是顺序翻转了。1.8之后就是尾部插入
下面给大家贴上部分我分析的源码 有注释
判断当前数组是否需要初始化。
如果 key 为空,则 put 一个空值进去。
根据 key 计算出 hashcode。
根据计算出的 hashcode 定位出所在桶。
如果桶是一个链表则需要遍历判断里面的 hashcode、key 是否和传入 key 相等,如果相等则进行覆盖,并返回原来的值。
如果桶是空的,说明当前位置没有数据存入;新增一个 Entry 对象写入当前位置。
public V put(K key, V value) {
2 if (table == EMPTY_TABLE) {
3 inflateTable(threshold);
4 }
5 if (key == null)
6 return putForNullKey(value);
7 int hash = hash(key);
8 int i = indexFor(hash, table.length);
9 for (Entry<K,V> e = table[i]; e != null; e = e.next) {
10 Object k;
11 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
12 V oldValue = e.value;
13 e.value = value;
14 e.recordAccess(this);
15 return oldValue;
16 }
17 }
18
19 modCount++;
20 addEntry(hash, key, value, i);
21 return null;
22 }
当调用 addEntry 写入 Entry 时需要判断是否需要扩容。
如果需要就进行两倍扩充,并将当前的 key 重新 hash 并定位。
而在 createEntry 中会将当前位置的桶传入到新建的桶中,如果当前桶有值就会在位置形成链表。
void addEntry(int hash, K key, V value, int bucketIndex) {
2 if ((size >= threshold) && (null != table[bucketIndex])) {
3 resize(2 * table.length);
4 hash = (null != key) ? hash(key) : 0;
5 bucketIndex = indexFor(hash, table.length);
6 }
7
8 createEntry(hash, key, value, bucketIndex);
9 }
10
11 void createEntry(int hash, K key, V value, int bucketIndex) {
12 Entry<K,V> e = table[bucketIndex];
13 table[bucketIndex] = new Entry<>(hash, key, value, e);
14 size++;
15 }
get 方法
再来看看 get 函数:
首先也是根据 key 计算出 hashcode,然后定位到具体的桶中。
判断该位置是否为链表。
不是链表就根据 key、key 的 hashcode 是否相等来返回值。
为链表则需要遍历直到 key 及 hashcode 相等时候就返回值。
啥都没取到就直接返回 null 。
1 public V get(Object key) {
2 if (key == null)
3 return getForNullKey();
4 Entry<K,V> entry = getEntry(key);
5
6 return null == entry ? null : entry.getValue();
7 }
8
9 final Entry<K,V> getEntry(Object key) {
10 if (size == 0) {
11 return null;
12 }
13
14 int hash = (key == null) ? 0 : hash(key);
15 for (Entry<K,V> e = table[indexFor(hash, table.length)];
16 e != null;
17 e = e.next) {
18 Object k;
19 if (e.hash == hash &&
20 ((k = e.key) == key || (key != null && key.equals(k))))
21 return e;
22 }
23 return null;
24 }
Base 1.8
不知道 1.7 的实现大家看出需要优化的点没有?
其实一个很明显的地方就是:
当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O(N)。
因此 1.8 中重点优化了这个查询效率。
1.8 HashMap 结构图:
先来看看几个核心的成员变量:
1 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
2
3 /**
4 * The maximum capacity, used if a higher value is implicitly specified
5 * by either of the constructors with arguments.
6 * MUST be a power of two <= 1<<30.
7 */
8 static final int MAXIMUM_CAPACITY = 1 << 30;
9
10 /**
11 * The load factor used when none specified in constructor.
12 */
13 static final float DEFAULT_LOAD_FACTOR = 0.75f;
14
15 static final int TREEIFY_THRESHOLD = 8;
16
17 transient Node<K,V>[] table;
18
19 /**
20 * Holds cached entrySet(). Note that AbstractMap fields are used
21 * for keySet() and values().
22 */
23 transient Set<Map.Entry<K,V>> entrySet;
24
25 /**
26 * The number of key-value mappings contained in this map.
27 */
28 transient int size;
和 1.7 大体上都差不多,还是有几个重要的区别:
TREEIFY_THRESHOLD 用于判断是否需要将链表转换为红黑树的阈值。
HashEntry 修改为 Node。
Node 的核心组成其实也是和 1.7 中的 HashEntry 一样,存放的都是 key value hashcode next 等数据。
再来看看核心方法。
put 方法
看似要比 1.7 的复杂,我们一步步拆解:
判断当前桶是否为空,空的就需要初始化(resize 中会判断是否进行初始化)。
根据当前 key 的 hashcode 定位到具体的桶中并判断是否为空,为空表明没有 Hash 冲突就直接在当前位置创建一个新桶即可。
如果当前桶有值( Hash 冲突),那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e,在第 8 步的时候会统一进行赋值及返回。
如果当前桶为红黑树,那就要按照红黑树的方式写入数据。
如果是个链表,就需要将当前的 key、value 封装成一个新节点写入到当前桶的后面(形成链表)。
接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树。
如果在遍历过程中找到 key 相同时直接退出遍历。
如果 e != null 就相当于存在相同的 key,那就需要将值覆盖。
最后判断是否需要进行扩容。
get 方法
1 public V get(Object key) {
2 Node<K,V> e;
3 return (e = getNode(hash(key), key)) == null ? null : e.value;
4 }
5
6 final Node<K,V> getNode(int hash, Object key) {
7 Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
8 if ((tab = table) != null && (n = tab.length) > 0 &&
9 (first = tab[(n - 1) & hash]) != null) {
10 if (first.hash == hash && // always check first node
11 ((k = first.key) == key || (key != null && key.equals(k))))
12 return first;
13 if ((e = first.next) != null) {
14 if (first instanceof TreeNode)
15 return ((TreeNode<K,V>)first).getTreeNode(hash, key);
16 do {
17 if (e.hash == hash &&
18 ((k = e.key) == key || (key != null && key.equals(k))))
19 return e;
20 } while ((e = e.next) != null);
21 }
22 }
23 return null;
24 }
get 方法看起来就要简单许多了。
首先将 key hash 之后取得所定位的桶。
如果桶为空则直接返回 null 。
否则判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 value。
如果第一个不匹配,则判断它的下一个是红黑树还是链表。
红黑树就按照树的查找方式返回值。
不然就按照链表的方式遍历匹配返回值。
从这两个核心方法(get/put)可以看出 1.8 中对大链表做了优化,修改为红黑树之后查询效率直接提高到了 O(logn)。
但是 HashMap 原有的问题也都存在,比如在并发场景下使用时容易出现死循环。
1final HashMap<String, String> map = new HashMap<String, String>();
2for (int i = 0; i < 1000; i++) {
3 new Thread(new Runnable() {
4 @Override
5 public void run() {
6 map.put(UUID.randomUUID().toString(), "");
7 }
8 }).start();
9}
HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。
下面就简单介绍 concurrenthashmap 这个我掌握的一般,因为它涉及好多东西,不会在这就能说完的,有些东西我也是 概念的了解,由这个集合引出的知识点我记得当初说了好多
说下concurrenthashmap 涉及的知识点 需要各位自己补充 1.7 1.8 区别 分段锁,sync重量锁(无所-偏向-轻量-重量)转换 cas机制以及ABA问题(
AtomicInteger
)(Unsafe 工具类的了解) AQS同步队列 (独占和共享两种
ReentrantLock 的实现就是基于 AQS
)
LockSuport pack unpack 和 Object.wait 的区别
以上集合介绍完毕
开始介绍下 MYSQL 以下部分先后,全是重点,目前只能想到这些
1.mysql的执行流程
2.mysql的 日志文件
3.mysql 索引的种类,索引失效情况,
4.不同存储引擎引发不同的索引情况
5. 索引存在底层文件格式以及数据结构,演变的由来,为什么
6.mysql 锁机制,以及mysql怎么解决的幻度(mvvc+锁)
7.mysql事务问题
8.explain 执行计划
下面再来说下 redis的吧,这块在大家面试互联网很重要,基本一个概念,redis 集群化已经做到数据基本不丢失,所以只有不是重要数据,业务都会考虑缓存。
1.必须懂 LRU
2. redis 结构类型 redisObject 结构是如何的,不了解redis数据结构【不是指的 string,list,set 之类,不懂建议百度】无法深入,比如一道面试题: redis 的 字符串 String 底层结构是什么 为啥不用 char 存储?
3.redis io 模型, 还是我之前在一个某某视频看到说是redis数据获取快其中有一部分原因是因为 跳表的数据结构,屁,跳表只存在于 zset数据长度超出 结构变化才是
4.redis aof rdb 删除策略,淘汰策略 以及哨兵,集群,配置文件优化 分布式锁,和zk的分布式锁的区别
下面是些redis常见的问题
问题:
怎么让一个原来的 slave 节点成为主节点?
redis分布式锁和zookeeper分布式锁的实现方案和对比
传统的LRU是 基于链表+HashMap实现的,redis是如何解决的 如何找出热度最低的数据?
常见Redis数据丢失的情况
- 程序bug或人为误操作
- 因客户端缓冲区内存使用过大,导致大量键被LRU淘汰
- 主库故障后自动重启,可能导致数据丢失
- 网络分区的问题,可能导致短时间的写入数据丢失
- 主从复制数据不一致,发生故障切换后,出现数据丢失
- 大量过期键,同时被淘汰清理
未完,待续,微服务项目知识点,分布式环境项目引发的问题,事务问题,锁失效等等,大家最好还是把 mybatis spirng 源码过一遍,他们对应应用了不同的设计模式,本人目前在 看 mybatis 萌萌哒