1. 自我介绍
2. 了解ConcurrentHashMap吗?
concurrentHashMap的底层数据结构:在JDK1.7时,concurrentHashMap底层是用数组+链表实现的,在JDK1.8时,底层数据结构是用数组+链表+红黑树实现的
在实现线程安全方面:concurrentHashMap在JDK1.7时,concurrentHashMap采用的是分段锁的设计方式,对整个桶数组进行分割,设置一个segment,每一把锁只锁容器的一部门数据,多线程访问容器里不同数据端的数据,就不会存在锁竞争,提高并发率。
在JDK1.8时,摒弃了segment的概念,直接用Node数组+链表+红黑树的数据结构来实现,在多线程并发时,实际是使用synchronize+CAS来操作的。并且synchronized锁在JDK1.7时对锁进行了优化,整个看起来就行优化过且线程安全的HashMap。
3. HashMap为啥每次扩容是原来的2倍
- HashMap实行了懒加载,新建HashMap的时候不会对table进行赋值,而是到第一次插入时,进行resize时构建table
- 当HashMap.size 大于计算好的阈值 threshold时, 会进行resize;threshold的值,当第一次构建时, 如果没有指定HashMap.table的初始长度, 就用默认值16, 否则就是指定的值; 然后不管是第一次构建还是后续扩容, threshold = table.length * loadFactor;
为什么是2倍扩容?
第一是因为哈希函数的问题,通过除留余数法方法获取桶号,因为哈希表的大小始终为2的n次幂,因此可以将取模为位运算操作,提高效率,容量n为2的幂次方,n-1的二进制会全为1,位运算可以充分散列,避免不必要的哈希冲突,这也就是为什么要按照2倍方式扩容的一个原因
第二是因为是否移位的问题
是否移位,由扩容后表示的最高位是否1为所决定,并且移动的方向只有一个,即向高位移动。因此,可以根据对最高位进行检测的结果来决定是否移位,从而可以优化性能,不用每一个元素都进行移位,因为为0说明刚好在移位完之后的位置,为1说明不是需要移动oldCop,这也是其为什么要按照2倍方式扩容的第二个原因。
4. ConcurrentHashMap的put操作过程
JDK1.7put的实现:
JDK1.7中引入Segment,Segment类通过继承ReentrantLock类,进行加锁,从而控制整个插入过程。
Segment数组也是一种数组加链表的结构方式,每个segment[i]都有一把锁,当某对<key,value>想要进行插入操作,首先要找对应segment数组对应的index,并获取锁,才能对HashEntry进行操作。
第一步:首先通过ConcurrentHashMap中的put方法,计算key.hash的值,根据hash值定位到segment索引;
第二步:进入到segment的put()方法,1.要上锁;2.计算出当前key.hash;3.确定hashEntry的位置,找到此hashEntry[i]的头节点first;3.1 此节点是否是null,若不是则需要遍历,3.2遍历完成或此节点为空,创建HashEntry,插入;3.3此处要注意判断当前segement的count数量,是否需要进行rehash;4 释放锁。
JDK1.8put的实现:
JDK1.8较之前版本进行了改进,采用分段+CAS锁的方式保证线程安全,分段锁是基于synchronized关键字实现的。
1.key或value是否为空,是的话,抛异常new NullPointerException(); 2.table是否为空或length0;是的话,初始化table; 3.根据key算出的hash值经过优化得到索引值i,如果i-1,说明此时有线程扩容此链表,你需要去帮忙扩容。 4.i>=0,则找到table[i]对应的索引,为空的话,就CAS添加; 5.table[i]不为空,取出节点锁住,表示锁住此索引的所有链表或红黑树。判断是key是否重复,重复的话就更新value,否者尾插入法,更新。如果是红黑树就红黑树插入。 6.插入后判断链表的节点数是否大于8,是的话,转换为红黑树, 7.最后判断concurrentHashMap容量,大于扩容值,就进行扩容。 https://blog.csdn.net/tianyuxingxuan/article/details/77434404
5. put里面什么情况下会用CAS,什么情况下用synchronized的机制
6. sleep(0)的作用是什么
指定零 (0) 以指示应挂起此线程以使其他等待线程能够执行。
Thread.Sleep(0) 并非是真的要线程挂起0毫秒,意义在于这次调用Thread.Sleep(0)的当前线程确实的被冻结了一下,让其他线程有机会优先执行。Thread.Sleep(0) 是你的线程暂时放弃cpu,也就是释放一些未用的时间片给其他线程或进程使用,就相当于一个让位动作。
在线程中,调用sleep(0)可以释放cpu时间,让线程马上重新回到就绪队列而非等待队列,sleep(0)释放当前线程所剩余的时间片(如果有剩余的话),这样可以让操作系统切换其他线程来执行,提升效率。
7. mysql默认事务隔离级别
可重复读
四大隔离级别:
未提交读——事务中的修改,即使没有提交,对其他事务也是可见的——脏读、不可重复读、幻读
可重复读——一个事务只能读取到已经提交的事务所做的修改——不可重复读、幻读
提交读——保证一个事务中多次读取同样的数据结果是一样的——幻读
串行化——强制串行化
8. 幻读和不可重复读的区别
幻读:同一事务下,连续执行两次同样的sql语句可能返回不同的结果,第二次的sql语句可能会返回之前不存在的行
不可重复读:同一事物多次读取同一数据集合,读取到数据不一样的情况
9. RR级别会出现幻读吗
会,
10. Mysql如何解决幻读的机制?
解决幻读是采用的Record Lock + Gap Lock解决
11. 说一下MVCC
MVCC,是多版本并发控制,主要为了去提升并发性能的考虑,通过行级锁的变种,避免加锁操作,减少开销,只适用于读已提交和可重复读两个隔离级别。主要是通过读取历史版本数据,来降低并发事务冲突,从而提升并发性能的一种机制。
12. MySQL MVCC具体实现方式是哪一种
13. 说一下TCP四次挥手,越详细越好
14. 为什么是四次挥手,最后一次如果不挥手会有问题吗?
15. 为什么是等待2MSL?为什么不是1MSL
16. Synchronized锁升级过程
synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问 的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进 入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象, 如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之 后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程 就构成了 synchronized 锁的升级。
17. 为什么要引入锁的升级机制?
锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而 减低了锁带来的性能消耗。
18. 偏向锁主要解决什么问题
偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。
轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。
19. 偏向锁是怎么加锁的?
偏向锁假定将来只有第一个申请锁的线程会使用锁(不会有任何线程再来申请锁),因此,只需要在Mark Word中CAS记录owner(本质上也是更新,但初始值为空),如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁,以后当前线程等于owner就可以零成本的直接获得锁;否则,说明有其他线程竞争,膨胀为轻量级锁。
|