java面试

ConCurrentHashMap,HashMap,HashTa

2020-03-03  本文已影响0人  Helloword_Cc

本文主要讲解一下ConCurrentHashMap,HashMap,HashTable的区别和ConCurrentHashMap的实现原理

通常我们使用的比较多的是HashMap,但HashMap线程不安全。所以我们使用HashTable/ConCurrentHashMap。但HashTable使用锁机制,就是get/put都使用锁住的方式,所以速度不理想。

区别:

HashMap:
1)key允许为null,源码做了特殊处理

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

2)线程不安全
3)因为HashMap 中的 Iterator 迭代器是 fail-fast,所以HashMap遍历的途中,有其他线程进行插入/删除操作导致数据的结构发生变化。则会抛出Concurrent Modification Exception

快速失败(fail—fast)是java集合中的一种机制, 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。

他的原理是啥?


迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。

集合在被遍历期间如果内容发生变化,就会改变modCount的值。
每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
Tip:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。

4)初始容量为11字节
5)扩容为当前容量翻倍

HashTable:
1)key不允许为null
2)线程安全
3)速度慢,因为所有操作都上了锁,保证同步。
4)初始容量为16字节
5)扩容为当前容量翻倍+1

ConCurrentHashMap:
1)key不允许为null
2)线程安全
3)使用分段锁来保证线程安全,而且速度比hashTable强很多。

接下来解析ConCurrentHashMap的原理

ConcurrentHashMap 底层是基于 数组 + 链表 组成的,不过在 jdk1.7 和 1.8 中具体实现稍有不同。
1.7中的数据结构:

image.png
如图所示,是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表。
Segment 是 ConcurrentHashMap 的一个内部类,主要的组成如下:
static final class Segment<K,V> extends ReentrantLock implements Serializable {   
 private static final long serialVersionUID = 2249069246763182397L;    
// 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶   
 transient volatile HashEntry<K,V>[] table;    transient int count;       
 // 记得快速失败(fail—fast)么?   
 transient int modCount;        
// 大小   
 transient int threshold;        
// 负载因子    
final float loadFactor;
}

HashEntry跟HashMap差不多的,但是不同点是,他使用volatile去修饰了他的数据Value还有下一个节点next。

volatile的特性是啥?

保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
禁止进行指令重排序。(实现有序性)
volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。

他并发度高的原因?

原理上来说,ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。
不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。
每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。
就是说如果容量大小是16他的并发度就是16,可以同时允许16个线程操作16个Segment而且还是线程安全的。

public V put(K key, V value) {   
 Segment<K,V> s;   
 if (value == null)        throw new NullPointerException();
//这就是为啥他不可以put null值的原因    
int hash = hash(key);    
int j = (hash >>> segmentShift) & segmentMask;   
 if ((s = (Segment<K,V>)UNSAFE.getObject                  
 (segments, (j << SSHIFT) + SBASE)) == null)       
  s = ensureSegment(j);    
return s.put(key, hash, value, false)
;}

他先定位到Segment,然后再进行put操作。

我们看看他的put源代码,你就知道他是怎么做到线程安全的了,关键句子我注释了。

 final V put(K key, int hash, V value, boolean onlyIfAbsent) {         
 // 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry           
 HashEntry<K,V> node = tryLock() ? null :                scanAndLockForPut(key, hash, value);            
V oldValue;           
 try {               
 HashEntry<K,V>[] tab = table;    
           int index = (tab.length - 1) & hash;          
      HashEntry<K,V> first = entryAt(tab, index);     
           for (HashEntry<K,V> e = first;;) { 
                   if (e != null) {                       
 K k; 
// 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。                       
 if ((k = e.key) == key || (e.hash == hash && key.equals(k))) {              
              oldValue = e.value;     
             if (!onlyIfAbsent) {                     
           e.value = value;                              
  ++modCount;                          
  }                          
  break;                        }              
          e = e.next;                    }        
            else {                
 // 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。                       
 if (node != null)                         
   node.setNext(first);                        
else                           
 node = new HashEntry<K,V>(hash, key, value, first);          
              int c = count + 1;                
        if (c > threshold && tab.length < MAXIMUM_CAPACITY)       
                     rehash(node);                    
    else                           
 setEntryAt(tab, index, node);      
                  ++modCount;                     
   count = c;                       
 oldValue = null;                       
 break;                 
   }            
    }         
   } finally {    
           //释放锁            
    unlock();          
  }          
  return oldValue;        }

首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。

尝试自旋获取锁。
如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。

那他get的逻辑呢?


get 逻辑比较简单,只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。
由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。
ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁。

**

上一篇下一篇

猜你喜欢

热点阅读