ConcurrentHashMap是hashMap在多线程安全下的实现,当hashMap要作为类成员变量时都可考虑使用concurrentHashMap来替代HahsMap.
如果使用的是无参构造器,那么在put时会对数组进行初始化,这个过程是线程不安全的
java1.7之前resize扩容时多线程会导致元素丢失或死环,1.8之后虽然不存在这个问题,但多线程下每个线程都是重复工作
tab[i]=new Node()操作在多线程下可能会导致元素覆盖
size++操作是非线程安全的,导致size不准确
- final V putVal(K key, V value, boolean onlyIfAbsent) {
- if (key == null || value == null) throw new NullPointerException();
- 获取hash值,这里跟hashmap不同的是多了个HASH_BITS,可能是为了更加散列减少hash冲突
- int hash = spread(key.hashCode());
- int binCount = 0;
- for (Node<K,V>[] tab = table;;) {
- Node<K,V> f; int n, i, fh;
- if (tab == null || (n = tab.length) == 0)
- 如果使用的无参构造器,那么这里就要先初始化数组,方法里面使用了compareAndSwapInt保证只有一个线程能进行数组初始化,且初始化容量默认是16,其他线程再次进入for循环
- tab = initTable();
- 当hash值对应的数组下标位置没有元素,也就是没有hash冲突时,cas保证同时只有一个线程能tab[i]操作成功,这就避免了可能的元素覆盖,操作成功的线程直接break退出,操作失败的线程再次进入for循环
- else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
- if (casTabAt(tab, i, null,
- new Node<K,V>(hash, key, value, null)))
- break; // no lock when adding to empty bin
- }
-
- else if ((fh = f.hash) == MOVED)
- tab = helpTransfer(tab, f);
- else {
- V oldVal = null;
- 当tab[i]元素不为null,说明有hash冲突,就用synchronized锁住tab[i]上的元素
- synchronized (f) {
- 双重判断,跟单例原理差不多,怕上锁前已被其他线程修改
- if (tabAt(tab, i) == f) {
- if (fh >= 0) {
- binCount = 1;
- 遍历hash冲突的链表,并用binCount记录元素个数
- for (Node<K,V> e = f;; ++binCount) {
- K ek;
- 判断key是不是一个,两个条件:hash相等且(key地址/值相等或key重写的equals相等),判断成立的话用新值覆盖老值并break
- if (e.hash == hash &&
- ((ek = e.key) == key ||
- (ek != null && key.equals(ek)))) {
- oldVal = e.val;
- if (!onlyIfAbsent)
- e.val = value;
- break;
- }
- Node<K,V> pred = e;
- 遍历到最后一个元素并把新元素挂在最后面
- if ((e = e.next) == null) {
- pred.next = new Node<K,V>(hash, key,
- value, null);
- break;
- }
- }
- }
- 如果是红黑树,则放入树中
- else if (f instanceof TreeBin) {
- Node<K,V> p;
- binCount = 2;
- if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
- value)) != null) {
- oldVal = p.val;
- if (!onlyIfAbsent)
- p.val = value;
- }
- }
- }
- }
- if (binCount != 0) {
- 如果链表个数超过了8个则转换为红黑树
- if (binCount >= TREEIFY_THRESHOLD)
- treeifyBin(tab, i);
- if (oldVal != null)
- return oldVal;
- break;
- }
- }
- }
- 这个实际就是size++操作和扩容判断,size++使用的是分段计数方式,对于每个分段使用cas进行+1,扩容过程跟put过程差不多,整个过程跟hashMap扩容原理一样,使用了尾插法,也使用synchronized锁,在tab[i]赋值时也使用了cas
- addCount(1L, binCount);
- return null;
- }
-
总结(如何解决了HashMap线程不安全问题):
在初始化数组时使用cas保证只有一个线程能成功并去做初始化工作,其他线程则继续for循环
在没有hash冲突时tab[i]使用cas进行赋值,保证同时只有一个线程赋值成功,其他线程则在下次for循环时进入hash冲突的代码段
当有hash冲突时使用synchronized锁住tab[i]位置的元素,而且进行了双检查,保证上锁后元素没有变化,然后在锁里进行链表或红黑树的维护,保证了hash冲突时元素的插入也是线程安全的
在size++时使用了分段计数方式,线程对单独的cell进行cas操作,这样减小了锁冲突的可能性,在保证线程安全的情况下提高了size++的性能,但在size()方法时需要把base和cell的数都加起来,是一种典型的空间换时间的操作
在扩容时使用的技巧跟put过程差不多,通过cas和synchronized保证线程安全,跟hashmap一样是尾插法和2倍容量