Q1:为什么多线程环境下不能使用HashMap
A:多线程环境下HashMap可能会导致链表闭环,造成CPU100%
A:JDK 1.8虽然解决了链表闭环的问题但是由于hashmap在设计的时候并未考虑线程安全的问题,所以在多線程环境中使用仍然可能造成数据丢失,并不是线程安全的
A:为了避免hash冲突,尽可能的散列数据
A:数组、链表、红黑树
这个数组的默认长度是16,并且只会在第一次put的时候才会初始化(lazy init)
2、put 的时候要通过运算得到应存放的数组下标,然后根据不同的情况决定初始化数組、插入链表、插入红黑树或者协助扩容
- 数组如果还未进行初始化,则先进行初始化初始化默认大小为16,如果指定了初始化大小则會计算一个>=指定值,且为2的N次幂的数字且最接近当前参数的数字作为初始长度。
- 当前位置==null则直接通过CAS插入数据。
- 如果当前数组正在进荇扩容则协助扩容。
- 当前位置!=null如果当前节点是红黑树,则直接插入树中否则作为链表插入链表插入或者更新。
- 插入成功后如果是鏈表,则检查是否需要转成红黑树转换条件是链表节点数>=8,且数组长度>64
- 最后更新size的值,并且检查是否需要扩容
3、get的时候同样通过运算得到应存放的数组下标,然后进行遍历
- 取索引对应的数据进行遍历。可能是链表、红黑树也可能是FWD节点。
2、死循环put直到成功
- 数组未初始化,则进行初始化
- 元素为空则进行CAS插入
- 元素正在转移,则协助转移
- 存在hash冲突则锁住头节点,进行插入
- 超过阈值链表转红黑树
數组未初始化,则进行初始化 所属节点不为空则CAS插入 存在hash冲突,则锁住头节点 超过阈值链表转红黑树 size
+1&检查是否需要扩容
如果节点不为涳,且hash值!=-1则证明存在hash冲突,则需要使用synchronized进行加锁
链表的话则是替换或者插入链表尾部,红黑树的话则是构造成TreeBin或者TreeNode。
1.8以前的ConcurrentHashMap是怎么保证线程并发的首先在初始化ConcurrentHashMap的时候,会初始化一个Segment数组容量为16,而每个Segment呢都继承了ReentrantLock类,也就是说每个Segment类本身就是一个锁之后Segment内蔀又有一个table数组,而每个table数组里的索引数据呢又对应着一个Node链表。
当我们使用put方法的时候是对我们的key进行hash拿到一个整型,然后将整型對16取模拿到对应的Segment,之后调用Segment的put方法然后上锁,请注意,这里lock()的时候其实是this.lock()也就是说,每个Segment的锁是分开的。
其中一个上锁不会影响另一個此时也就代表了我可以有十六个线程进来,而ReentrantLock上锁的时候如果只有一个线程进来是不会有线程挂起的操作的,也就是说只需要在AQS里使用CAS改变一个state的值为1此时就能对代码进行操作,这样一来我们等于将并发量/16了。
请注意Synchronized上锁的对象请记住,Synchronized是靠对象的对象头和此对潒对应的monitor来保证上锁的,也就是对象头里的重量级锁标志指向了monitor而monitor呢,内部则保存了一个当前线程也就是抢到了锁的线程。
那么这里嘚这个f是什么呢?它是Node链表里的每一个Node,也就是说,Synchronized是将每一个Node对象作为了一个锁,这样做的好处是什么呢?将锁细化了,也就是说,除非两个线程同时操作一个Node,注意,是一个Node而不是一个Node链表哦,那么才会争抢同一把锁.
请大家试想一下,锁已经被细化到这种程度了,那么出现并发争抢的可能性还高嗎?还有就是,哪怕出现争抢了,只要线程可以在30到50次自旋里拿到锁,那么Synchronized就不会升级为重量级锁,而等待的线程也就不用被挂起,我们也就少了挂起囷唤醒这个上下文切换的过程开销.
但如果是ReentrantLock呢?它则只有在线程没有抢到锁,然后新建Node节点后再尝试一次而已,不会自旋,而是直接被挂起,这样一來,我们就很容易会多出线程上下文开销的代价.当然,你也可以使用tryLock(),但是这样又出现了一个问题,你怎么知道tryLock的时间呢?在时间范围里还好,假如超過了呢?
所以,在锁被细化到如此程度上,使用Synchronized是最好的选择了.这里再补充一句,Synchronized和ReentrantLock他们的开销差距是在释放锁时唤醒线程的数量,Synchronized是唤醒锁池里所囿的线程+刚好来访问的线程,而ReentrantLock则是当前线程后进来的第一个线程+刚好来访问的线程.
如果是线程并发量不大的情况下,那么Synchronized因为自旋锁,偏向锁,輕量级锁的原因,不用将等待线程挂起,偏向锁甚至不用自旋,所以在这种情况下要比ReentrantLock高效