`
wuhuajun
  • 浏览: 93649 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

HashMap&ConcurrentHashMap 深度分析(转)

    博客分类:
  • java
 
阅读更多

java.util.HashMap是很常见的类,前段时间公司系统由于对HashMap使用不当,导致cpu百分之百,在并发环境下使用HashMap 而没有做同步,可能会引起死循环,关于这一点,sun的官方网站上已有阐述,这并非是bug。

HashMap的数据结构
         HashMap主要是用数组来存储数据的,我们都知道它会对key进行哈希运算,哈系运算会有重复的哈希值,对于哈希值的冲突,HashMap采用链表来解决的。在HashMap里有这样的一句属性声明:
transient Entry[] table;
Entry就是HashMap存储数据所用的类,它拥有的属性如下
final K key;
V value;
final int hash;
Entry<K,V> next;

看到next了吗?next就是为了哈希冲突而存在的。比如通过哈希运算,一个新元素应该在数组的第10个位置,但是第10个位置已经有Entry,那么好吧,将新加的元素也放到第10个位置,将第10个位置的原有Entry赋值给当前新加的 Entry的next属性。数组存储的是链表,链表是为了解决哈希冲突的,这一点要注意。


几个关键的属性
存储数据的数组
transient Entry[] table; 
这个上面已经讲到了
默认容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
默认加载因子,加载因子是一个比例,当HashMap的数据大小>=容量*加载因子时,HashMap会将容量扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
当实际数据大小超过threshold时,HashMap会将容量扩容,threshold=容量*加载因子
int threshold;
加载因子
final float loadFactor;


HashMap的初始过程
构造函数1

Java代码 复制代码 收藏代码
  1. public HashMap(int initialCapacity, float loadFactor) {   
  2.     if (initialCapacity < 0)   
  3.         throw new IllegalArgumentException("Illegal initial capacity: " +   
  4.                                            initialCapacity);   
  5.     if (initialCapacity > MAXIMUM_CAPACITY)   
  6.         initialCapacity = MAXIMUM_CAPACITY;   
  7.     if (loadFactor <= 0 || Float.isNaN(loadFactor))   
  8.         throw new IllegalArgumentException("Illegal load factor: " +   
  9.                                            loadFactor);   
  10.   
  11.     // Find a power of 2 >= initialCapacity   
  12.     int capacity = 1;   
  13.     while (capacity < initialCapacity)   
  14.         capacity <<= 1;   
  15.   
  16.     this.loadFactor = loadFactor;   
  17.     threshold = (int)(capacity * loadFactor);   
  18.     table = new Entry[capacity];   
  19.     init();   
  20. }  

 



重点注意这里 

Java代码 复制代码 收藏代码
  1. while (capacity < initialCapacity)   
  2.             capacity <<= 1;  

 


capacity才是初始容量,而不是initialCapacity,这个要特别注意,如果执行new HashMap(9,0.75);那么HashMap的初始容量是16,而不是9,想想为什么吧。2的四次方16  容量不是根据传入的值 而是根据次方一直增上去。直到大于传入的值为止。

构造函数2

Java代码 复制代码 收藏代码
  1. public HashMap(int initialCapacity) {   
  2.         this(initialCapacity, DEFAULT_LOAD_FACTOR);   
  3.     }  

 


构造函数3,全部都是默认值

Java代码 复制代码 收藏代码
  1. public HashMap() {   
  2.      this.loadFactor = DEFAULT_LOAD_FACTOR;   
  3.      threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);   
  4.      table = new Entry[DEFAULT_INITIAL_CAPACITY];   
  5.      init();   
  6.  }  

 


构造函数4

Java代码 复制代码 收藏代码
  1. public HashMap(Map<? extends K, ? extends V> m) {   
  2.       this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,   
  3.                     DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);   
  4.       putAllForCreate(m);   
  5.   }  

 



如何哈希
        HashMap并不是直接将对象的hashcode作为哈希值的,而是要把key的hashcode作一些运算以得到最终的哈希值,并且得到的哈希值也不是在数组中的位置哦,无论是get还是put还是别的方法,计算哈希值都是这一句:
int hash = hash(key.hashCode());
hash函数如下:

Java代码 复制代码 收藏代码
  1. static int hash(int h) {   
  2.   return useNewHash ? newHash(h) : oldHash(h);   
  3.   }  

 


useNewHash声明如下:
  

Java代码 复制代码 收藏代码
  1. private static final boolean useNewHash;   
  2.    static { useNewHash = false; }  

 

这说明useNewHash其实一直为false且不可改变的,hash函数里对 useNewHash的判断真是多余的。

Java代码 复制代码 收藏代码
  1. private static int oldHash(int h) {  
  2.     h += ~(h << 9);   
  3.     h ^=  (h >>> 14);   
  4.     h +=  (h << 4);   
  5.     h ^=  (h >>> 10);   
  6.     return h;   
  7. }   
  8.   
  9. private static int newHash(int h) {   
  10.     // This function ensures that hashCodes that differ only by   
  11.     // constant multiples at each bit position have a bounded   
  12.     // number of collisions (approximately 8 at default load factor).   
  13.     h ^= (h >>> 20) ^ (h >>> 12);   
  14.     return h ^ (h >>> 7) ^ (h >>> 4);   
  15. }  

 



其实HashMap的哈希函数会一直都是oldHash。


如果确定数据的位置
看下面两行
  

Java代码 复制代码 收藏代码
  1. int hash = hash(k.hashCode());   
  2.   int i = indexFor(hash, table.length);  

 

第一行,上面讲过了,是得到哈希值,第二行,则是根据哈希指计算元素在数组中的位置了,位置的计算是将哈希值和数组长度按位与运算。

Java代码 复制代码 收藏代码
  1. static int indexFor(int h, int length) {   
  2.      return h & (length-1);   //这个算法的作用 应该就是能尽量把值散开,hash到不同的数组索引上去。
  3.  }  

 

 

“h & (length-1)”其实这里是很有讲究的,为什么是和(length-1)进行按位与运算呢?这样做是为了提高HashMap的效率。什么?这样能提高效率?且听我细细道来。

首先我们要确定一下,HashMap的数组长度永远都是偶数,即使你在初始化的时候是这样的new HashMap(15,0.75);因为在构造函数内部,上面也讲过,有这样的一段代码:

Java代码 复制代码 收藏代码
  1. while (capacity < initialCapacity)   
  2.             capacity <<= 1;  

 

 

所以length-1一定是个奇数,假设现在长度为16,减去1后就是15,对应的二进制是:1111。

假设有两个元素,一个哈希值是8,二进制是1000,一个哈希值是9,二进制是1001。和1111与运算后,分别还是1000和1001,它们被分配在了数组的不同位置,这样,哈希的分布非常均匀。

那么,如果数组长度是奇数,减去1后就是偶数了,偶数对应的二进制最低位一定是0了,例如14二进制1110。对上面两个数子分别与运算,得到1000和1000。看到了吗?都是一样的值,哈希值8和9的元素多被存储在数组同一个位置的链表中。在操作的时候,链表中的元素越多,效率越低,因为要不停的对链表循环比较。所以,一定要哈希均匀分布,尽量减少哈希冲突,减少了哈希冲突,就减少了链表循环,就提高了效率。

 

 

 


put方法到底作了什么?

Java代码 复制代码 收藏代码
  1. public V put(K key, V value) {   
  2.  if (key == null)   
  3.      return putForNullKey(value);   
  4.      int hash = hash(key.hashCode());   
  5.      int i = indexFor(hash, table.length);   
  6.      for (Entry<K,V> e = table[i]; e != null; e = e.next) {   
  7.          Object k;   
  8.          if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {   
  9.              V oldValue = e.value;   
  10.              e.value = value;   
  11.              e.recordAccess(this);   
  12.              return oldValue;   
  13.          }   
  14.      }   
  15.   
  16.      modCount++;   
  17.      addEntry(hash, key, value, i);   
  18.      return null;   
  19.  }  

 


如果key为NULL,则是单独处理的,看看putForNullKey方法:
  

Java代码 复制代码 收藏代码
  1. private V putForNullKey(V value) {   
  2.       int hash = hash(NULL_KEY.hashCode());   
  3.       int i = indexFor(hash, table.length);   
  4.   
  5.       for (Entry<K,V> e = table[i]; e != null; e = e.next) {   
  6.           if (e.key == NULL_KEY) {   
  7.               V oldValue = e.value;   
  8.               e.value = value;   
  9.               e.recordAccess(this);   
  10.               return oldValue;   
  11.           }   
  12.       }   
  13.   
  14.       modCount++;   
  15.       addEntry(hash, (K) NULL_KEY, value, i);   
  16.       return null;   
  17.   }  

 

NULL_KEY的声明:static final Object NULL_KEY = new Object();
这一段代码是处理哈希冲突的,就是说,在数组某个位置的对象可能并不是唯一的,它是一个链表结构,根据哈希值找到链表后,还要对链表遍历,找出key相等的对象,替换它,并且返回旧的值。
   

Java代码 复制代码 收藏代码
  1. for (Entry<K,V> e = table[i]; e != null; e = e.next) {   
  2.          if (e.key == NULL_KEY) {   
  3.              V oldValue = e.value;   
  4.              e.value = value;   
  5.              e.recordAccess(this);   
  6.              return oldValue;   
  7.          }   
  8.      }  

 

如果遍历完了该位置的链表都没有找到有key相等的,那么将当前对象增加到链表里面去

Java代码 复制代码 收藏代码
  1. modCount++;   
  2. addEntry(hash, (K) NULL_KEY, value, i);   
  3. return null;  

 

且看看addEntry方法

Java代码 复制代码 收藏代码
  1. void addEntry(int hash, K key, V value, int bucketIndex) {   
  2. Entry<K,V> e = table[bucketIndex];   
  3.     table[bucketIndex] = new Entry<K,V>(hash, key, value, e);   
  4.     if (size++ >= threshold)   
  5.         resize(2 * table.length);   
  6. }  

 

table[bucketIndex] = new Entry<K,V>(hash, key, value, e);新建一个Entry对象,并放在当前位置的Entry链表的头部,看看下面的 Entry构造函数就知道了,注意红色部分。

Java代码 复制代码 收藏代码
  1. Entry(int h, K k, V v, Entry<K,V> n) {   
  2.        value = v;   
  3.        next = n;   
  4.        key = k;   
  5.        hash = h;   
  6.    }  

 


如何扩容?
        当put一个元素时,如果达到了容量限制,HashMap就会扩容,新的容量永远是原来的2倍。
上面的put方法里有这样的一段:

Java代码 复制代码 收藏代码
  1. if (size++ >= threshold)   
  2.             resize(2 * table.length);  

 

这是扩容判断,要注意,并不是数据尺寸达到HashMap的最大容量时才扩容,而是达到 threshold指定的值时就开始扩容, threshold=最大容量*加载因子。 看看resize方法
  

Java代码 复制代码 收藏代码
  1. void resize(int newCapacity) {   
  2.       Entry[] oldTable = table;   
  3.       int oldCapacity = oldTable.length;   
  4.       if (oldCapacity == MAXIMUM_CAPACITY) {   
  5.           threshold = Integer.MAX_VALUE;   
  6.           return;   
  7.       }   
  8.   
  9.       Entry[] newTable = new Entry[newCapacity];   
  10.       transfer(newTable);   
  11.       table = newTable;   
  12.       threshold = (int)(newCapacity * loadFactor);   
  13.   }  

 

重点看看红色部分的 transfer方法
  

Java代码 复制代码 收藏代码
  1. void transfer(Entry[] newTable) {   
  2.       Entry[] src = table;   
  3.       int newCapacity = newTable.length;   
  4.       for (int j = 0; j < src.length; j++) {   
  5.           Entry<K,V> e = src[j];   
  6.           if (e != null) {   
  7.               src[j] = null;   
  8.               do {   
  9.                   Entry<K,V> next = e.next;   
  10.                   int i = indexFor(e.hash, newCapacity);   
  11.                   e.next = newTable[i];   
  12.                   newTable[i] = e;   
  13.                   e = next;   
  14.               } while (e != null);   
  15.           }   
  16.       }   
  17.   }  

 

tranfer方法将所有的元素重新哈希,因为新的容量变大,所以每个元素的哈希值和位置都是不一样的。

 

如何寻找元素

       通过上面代码的分析,应该可以很清楚的看到,HashMap的元素查找大致分为三步:

根据key的hasocde()得到哈希值

根据哈希值确定元素在数组中的位置

找到指定位置的链表,循环比较,先“==”比较,如果不等,再“equals”比较,如果有一个比较相等,就说明找到元素了。

所以说到这里,我想大家也明白了,为什么要把一个对象放进HashMap的时候,最好是重写hashcode()方法和equals 方法呢?根据前面的分析,hashcode()可以确定元素在数组中的位置,而equals方法在链表的比较时要用到。

 

正确的使用HashMap
1:不要在并发场景中使用HashMap
           HashMap是线程不安全的,如果被多个线程共享的操作,将会引发不可预知的问题,据sun的说法,在扩容时,会引起链表的闭环,在get元素时,就会无限循环,后果是cpu100%。
看看get方法的红色部分

Java代码 复制代码 收藏代码
  1. public V get(Object key) {   
  2.     if (key == null)   
  3.         return getForNullKey();   
  4.         int hash = hash(key.hashCode());   
  5.         for (Entry<K,V> e = table[indexFor(hash, table.length)];   
  6.              e != null;   
  7.              e = e.next) {   
  8.             Object k;   
  9.             if (e.hash == hash && ((k = e.key) == key || key.equals(k)))   
  10.                 return e.value;   
  11.         }   
  12.         return null;   
  13.     }  

 


2:如果数据大小是固定的,那么最好给HashMap设定一个合理的容量值
        根据上面的分析,HashMap的初始默认容量是16,默认加载因子是0.75,也就是说,如果采用HashMap的默认构造函数,当增加数据时,数据实际容量超过10*0.75=12时,HashMap就扩容,扩容带来一系列的运算,新建一个是原来容量2倍的数组,对原有元素全部重新哈希,如果你的数据有几千几万个,而用默认的HashMap构造函数,那结果是非常悲剧的,因为HashMap不断扩容,不断哈希,在使用HashMap的场景里,不会是多个线程共享一个HashMap,除非对HashMap包装并同步,由此产生的内存开销和cpu开销在某些情况下可能是致命的。

 

 

 

 

==========在扩容时,会引起链表的闭环,在get元素时,就会无限循环,后果是cpu100%。========================

死锁是扩容操作与put或get方法并发引起的,看源码: 

引用

void transfer(Entry[] newTable) { 
        Entry[] src = table; 
        int newCapacity = newTable.length; 
        for (int j = 0; j < src.length; j++) { 
            Entry<K,V> e = src[j]; 
            if (e != null) { 
                src[j] = null; 
                do { 
                    Entry<K,V> next = e.next; 
                    int i = indexFor(e.hash, newCapacity); 
                    e.next = newTable[i]; 
                    newTable[i] = e;
//切换2 
                    e = next; 
                } while (e != null); 
            } 
        } 
    } 



//get() 

Java代码 复制代码 收藏代码
  1. for (Entry<K,V> e = table[indexFor(hash, table.length)];//切换1   
  2.              e != null;   
  3.              e = e.next)   



table初始状态: 
 
假设执行get的线程1在e = table[indexFor(hash, table.length)]后进行“切换1”,切换到线程2的put方法,而且进行了扩容。此时table和newTable的状态是: 
 
线程2在newTable[i] = e后进行“切换2”,继续执行线程1的get,在for循环里e.next==e,永远不为空,若 

Java代码 复制代码 收藏代码
  1. if (e.hash == hash && ((k = e.key) == key || key.equals(k)))  

 

不满足,则进入了死循环。

 

 

Java并发编程之ConcurrentHashMap

ConcurrentHashMap

ConcurrentHashMap是一个线程安全的Hash Table,它的主要功能是提供了一组和HashTable功能相同但是线程安全的方法。ConcurrentHashMap可以做到读取数据不加锁,并且其内部的结构可以让其在进行写操作的时候能够将锁的粒度保持地尽量地小,不用对整个ConcurrentHashMap加锁。

ConcurrentHashMap的内部结构

ConcurrentHashMap为了提高本身的并发能力,在内部采用了一个叫做Segment的结构,一个Segment其实就是一个类Hash Table的结构,Segment内部维护了一个链表数组,我们用下面这一幅图来看下ConcurrentHashMap的内部结构:
图表1
从上面的结构我们可以了解到,ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部,因此,这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长,但是带来的好处是写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment,这样,在最理想的情况下,ConcurrentHashMap可以最高同时支持Segment数量大小的写操作(刚好这些写操作都非常平均地分布在所有的Segment上),所以,通过这一种结构,ConcurrentHashMap的并发能力可以大大的提高。

Segment

我们再来具体了解一下Segment的数据结构:

 

Java代码 复制代码 收藏代码
  1. static final class Segment<K,V> extends ReentrantLock implements Serializable {   
  2.     transient volatile int count;   
  3.     transient int modCount;   
  4.     transient int threshold;   
  5.     transient volatile HashEntry<K,V>[] table;   
  6.     final float loadFactor;   
  7. }  
 

 

详细解释一下Segment里面的成员变量的意义:

  • count:Segment中元素的数量
  • modCount:对table的大小造成影响的操作的数量(比如put或者remove操作)
  • threshold:阈值,Segment里面元素的数量超过这个值依旧就会对Segment进行扩容
  • table:链表数组,数组中的每一个元素代表了一个链表的头部
  • loadFactor:负载因子,用于确定threshold

HashEntry

Segment中的元素是以HashEntry的形式存放在链表数组中的,看一下HashEntry的结构:

 

Java代码 复制代码 收藏代码
  1. static final class HashEntry<K,V> {   
  2.     final K key;   
  3.     final int hash;   
  4.     volatile V value;   
  5.     final HashEntry<K,V> next;   
  6. }  
 

可以看到HashEntry的一个特点,除了value以外,其他的几个变量都是final的,这样做是为了防止链表结构被破坏,出现ConcurrentModification的情况。

ConcurrentHashMap的初始化

下面我们来结合源代码来具体分析一下ConcurrentHashMap的实现,先看下初始化方法:

 

Java代码 复制代码 收藏代码
  1. public ConcurrentHashMap(int initialCapacity,   
  2.                          float loadFactor, int concurrencyLevel) {   
  3.     if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)   
  4.         throw new IllegalArgumentException();   
  5.     
  6.     if (concurrencyLevel > MAX_SEGMENTS)   
  7.         concurrencyLevel = MAX_SEGMENTS;   
  8.     
  9.     // Find power-of-two sizes best matching arguments   
  10.     int sshift = 0;   
  11.     int ssize = 1;   
  12.     while (ssize < concurrencyLevel) {   
  13.         ++sshift;   
  14.         ssize <<= 1;   
  15.     }   
  16.     segmentShift = 32 - sshift;   
  17.     segmentMask = ssize - 1;   
  18.     this.segments = Segment.newArray(ssize);   
  19.     
  20.     if (initialCapacity > MAXIMUM_CAPACITY)   
  21.         initialCapacity = MAXIMUM_CAPACITY;   
  22.     int c = initialCapacity / ssize;   
  23.     if (c * ssize < initialCapacity)   
  24.         ++c;   
  25.     int cap = 1;   
  26.     while (cap < c)   
  27.         cap <<= 1;   
  28.     
  29.     for (int i = 0; i < this.segments.length; ++i)   
  30.         this.segments[i] = new Segment<K,V>(cap, loadFactor);   
  31. }  
 

CurrentHashMap的初始化一共有三个参数,一个initialCapacity,表示初始的容量,一个loadFactor,表示负载参数,最后一个是concurrentLevel,代表ConcurrentHashMap内部的Segment的数量,ConcurrentLevel一经指定,不可改变,后续如果ConcurrentHashMap的元素数量增加导致ConrruentHashMap需要扩容,ConcurrentHashMap不会增加Segment的数量,而只会增加Segment中链表数组的容量大小,这样的好处是扩容过程不需要对整个ConcurrentHashMap做rehash,而只需要对Segment里面的元素做一次rehash就可以了。

整个ConcurrentHashMap的初始化方法还是非常简单的,先是根据concurrentLevel来new出Segment,这里Segment的数量是不大于concurrentLevel的最大的2的指数,就是说Segment的数量永远是2的指数个,这样的好处是方便采用移位操作来进行hash,加快hash的过程。接下来就是根据intialCapacity确定Segment的容量的大小,每一个Segment的容量大小也是2的指数,同样使为了加快hash的过程。

这边需要特别注意一下两个变量,分别是segmentShift和segmentMask,这两个变量在后面将会起到很大的作用,假设构造函数确定了Segment的数量是2的n次方,那么segmentShift就等于32减去n,而segmentMask就等于2的n次方减一。

ConcurrentHashMap的get操作

前面提到过ConcurrentHashMap的get操作是不用加锁的,我们这里看一下其实现:

 

Java代码 复制代码 收藏代码
  1. public V get(Object key) {   
  2.     int hash = hash(key.hashCode());   
  3.     return segmentFor(hash).get(key, hash);   
  4. }  
 

看第三行,segmentFor这个函数用于确定操作应该在哪一个segment中进行,几乎对ConcurrentHashMap的所有操作都需要用到这个函数,我们看下这个函数的实现:

 

Java代码 复制代码 收藏代码
  1. final Segment<K,V> segmentFor(int hash) {   
  2.     return segments[(hash >>> segmentShift) & segmentMask];   
  3. }  
 

这个函数用了位操作来确定Segment,根据传入的hash值向右无符号右移segmentShift位,然后和segmentMask进行与操作,结合我们之前说的segmentShift和segmentMask的值,就可以得出以下结论:假设Segment的数量是2的n次方,根据元素的hash值的高n位就可以确定元素到底在哪一个Segment中。

在确定了需要在哪一个segment中进行操作以后,接下来的事情就是调用对应的Segment的get方法:

 

Java代码 复制代码 收藏代码
  1. V get(Object key, int hash) {   
  2.     if (count != 0) { // read-volatile   
  3.         HashEntry<K,V> e = getFirst(hash);   
  4.         while (e != null) {   
  5.             if (e.hash == hash && key.equals(e.key)) {   
  6.                 V v = e.value;   
  7.                 if (v != null)   
  8.                     return v;   
  9.                 return readValueUnderLock(e); // recheck   
  10.             }   
  11.             e = e.next;   
  12.         }   
  13.     }   
  14.     return null;   
  15. }  
 

先看第二行代码,这里对count进行了一次判断,其中count表示Segment中元素的数量,我们可以来看一下count的定义:

 

Java代码 复制代码 收藏代码
  1. transient volatile int count;  
 

可以看到count是volatile的,实际上这里里面利用了volatile的语义:

 

 写道
对volatile字段的写入操作happens-before于每一个后续的同一个字段的读操作。

因为实际上put、remove等操作也会更新count的值,所以当竞争发生的时候,volatile的语义可以保证写操作在读操作之前,也就保证了写操作对后续的读操作都是可见的,这样后面get的后续操作就可以拿到完整的元素内容。

然后,在第三行,调用了getFirst()来取得链表的头部:

 

Java代码 复制代码 收藏代码
  1. HashEntry<K,V> getFirst(int hash) {   
  2.     HashEntry<K,V>[] tab = table;   
  3.     return tab[hash & (tab.length - 1)];   
  4. }  
 

同样,这里也是用位操作来确定链表的头部,hash值和HashTable的长度减一做与操作,最后的结果就是hash值的低n位,其中n是HashTable的长度以2为底的结果。

在确定了链表的头部以后,就可以对整个链表进行遍历,看第4行,取出key对应的value的值,如果拿出的value的值是null,则可能这个key,value对正在put的过程中,如果出现这种情况,那么就加锁来保证取出的value是完整的,如果不是null,则直接返回value。

ConcurrentHashMap的put操作

看完了get操作,再看下put操作,put操作的前面也是确定Segment的过程,这里不再赘述,直接看关键的segment的put方法:

 

Java代码 复制代码 收藏代码
  1. V put(K key, int hash, V value, boolean onlyIfAbsent) {   
  2.     lock();   
  3.     try {   
  4.         int c = count;   
  5.         if (c++ > threshold) // ensure capacity   
  6.             rehash();   
  7.         HashEntry<K,V>[] tab = table;   
  8.         int index = hash & (tab.length - 1);   
  9.         HashEntry<K,V> first = tab[index];   
  10.         HashEntry<K,V> e = first;   
  11.         while (e != null && (e.hash != hash || !key.equals(e.key)))   
  12.             e = e.next;   
  13.     
  14.         V oldValue;   
  15.         if (e != null) {   
  16.             oldValue = e.value;   
  17.             if (!onlyIfAbsent)   
  18.                 e.value = value;   
  19.         }   
  20.         else {   
  21.             oldValue = null;   
  22.             ++modCount;   
  23.             tab[index] = new HashEntry<K,V>(key, hash, first, value);   
  24.             count = c; // write-volatile   
  25.         }   
  26.         return oldValue;   
  27.     } finally {   
  28.         unlock();   
  29.     }   
  30. }  
 

首先对Segment的put操作是加锁完成的,然后在第五行,如果Segment中元素的数量超过了阈值(由构造函数中的loadFactor算出)这需要进行对Segment扩容,并且要进行rehash,关于rehash的过程大家可以自己去了解,这里不详细讲了。

第8和第9行的操作就是getFirst的过程,确定链表头部的位置。

第11行这里的这个while循环是在链表中寻找和要put的元素相同key的元素,如果找到,就直接更新更新key的value,如果没有找到,则进入21行这里,生成一个新的HashEntry并且把它加到整个Segment的头部,然后再更新count的值。

ConcurrentHashMap的remove操作

Remove操作的前面一部分和前面的get和put操作一样,都是定位Segment的过程,然后再调用Segment的remove方法:

 

Java代码 复制代码 收藏代码
  1. V remove(Object key, int hash, Object value) {   
  2.     lock();   
  3.     try {   
  4.         int c = count - 1;   
  5.         HashEntry<K,V>[] tab = table;   
  6.         int index = hash & (tab.length - 1);   
  7.         HashEntry<K,V> first = tab[index];   
  8.         HashEntry<K,V> e = first;   
  9.         while (e != null && (e.hash != hash || !key.equals(e.key)))   
  10.             e = e.next;   
  11.     
  12.         V oldValue = null;   
  13.         if (e != null) {   
  14.             V v = e.value;   
  15.             if (value == null || value.equals(v)) {   
  16.                 oldValue = v;   
  17.                 // All entries following removed node can stay   
  18.                 // in list, but all preceding ones need to be   
  19.                 // cloned.   
  20.                 ++modCount;   
  21.                 HashEntry<K,V> newFirst = e.next;   
  22.                 for (HashEntry<K,V> p = first; p != e; p = p.next)   
  23.                     newFirst = new HashEntry<K,V>(p.key, p.hash,   
  24.                                                   newFirst, p.value);   
  25.                 tab[index] = newFirst;   
  26.                 count = c; // write-volatile   
  27.             }   
  28.         }   
  29.         return oldValue;   
  30.     } finally {   
  31.         unlock();   
  32.     }   
  33. }  
 

首先remove操作也是确定需要删除的元素的位置,不过这里删除元素的方法不是简单地把待删除元素的前面的一个元素的next指向后面一个就完事了,我们之前已经说过HashEntry中的next是final的,一经赋值以后就不可修改,在定位到待删除元素的位置以后,程序就将待删除元素前面的那一些元素全部复制一遍,然后再一个一个重新接到链表上去,看一下下面这一幅图来了解这个过程:
1
假设链表中原来的元素如上图所示,现在要删除元素3,那么删除元素3以后的链表就如下图所示:
2

ConcurrentHashMap的size操作

在前面的章节中,我们涉及到的操作都是在单个Segment中进行的,但是ConcurrentHashMap有一些操作是在多个Segment中进行,比如size操作,ConcurrentHashMap的size操作也采用了一种比较巧的方式,来尽量避免对所有的Segment都加锁。

前面我们提到了一个Segment中的有一个modCount变量,代表的是对Segment中元素的数量造成影响的操作的次数,这个值只增不减,size操作就是遍历了两次Segment,每次记录Segment的modCount值,然后将两次的modCount进行比较,如果相同,则表示期间没有发生过写入操作,就将原先遍历的结果返回,如果不相同,则把这个过程再重复做一次,如果再不相同,则就需要将所有的Segment都锁住,然后一个一个遍历了,具体的实现大家可以看ConcurrentHashMap的源码,这里就不贴了。

分享到:
评论

相关推荐

    HashMap&ConcurrentHashMap.key

    HashMap& ConcurrentHashMap 深度解析

    ConcurrentHashMap共18页.pdf.zip

    综上所述,"ConcurrentHashMap共18页.pdf.zip"这份文档很可能是深入分析 ConcurrentHashMap 的详细指南,涵盖了其设计原理、实现机制以及最佳实践。如果你对并发编程或者Java集合框架有深入需求,这份资料将是一份...

    蚂蚁java架构师(第七/八期含项目) |课件完整|完结无秘

    08HashMap与ConcurrenthashMap源码解读 09MySQL深度原理解析 10Netty深度源码解读 11SpringCloud微服务框架源码解读 12彻底搞懂分布式锁架构设计原理 13分布式数据一致性设计原理 14分布式消息中间件 15实战新零售...

    通过面试题带你了解java的Map

    【标题】: "Java Map深度解析:从面试题看HashMap与ConcurrentHashMap" 【描述】: 本资源针对Java后端开发人员,由有7年大厂经验的专家精心整理,通过一系列面试题目来深入剖析Java中的Map,特别是HashMap和...

    java集合深度学习

    总的来说,理解HashMap和HashTable之间的差异是Java集合框架深度学习的关键部分。在选择使用哪个类时,需要根据具体的应用场景和性能需求来决定。同时,深入理解这些类的内部实现,有助于编写更高效、更安全的Java...

    Java集合框架深度解析:Map, List, Set

    首先,深入分析了HashMap的内部结构,包括它的数组+链表+红黑树的数据结构。重要的是理解如何处理并发问题,特别是在Java 8中对HashMap的优化,如高低位拆分转移方式,避免了JDK 7中出现的链表环问题。...

    实战java高并发编程

    Java并发库中的并发容器也是本书的重点,包括ArrayList、LinkedList、HashMap、ConcurrentHashMap等,并分析在并发环境下的优缺点和使用场景。此外,原子类如AtomicInteger、AtomicLong以及并发工具类如...

    Java常见笔试面试题目深度剖析

    - Map接口及其实现类:熟悉HashMap、TreeMap、ConcurrentHashMap等Map的特性和应用。 - 泛型:学习泛型的基本概念,如何声明和使用泛型类、泛型方法,以及类型擦除的概念。 4. **多线程** - 线程的创建:掌握...

    Java面试题(最全,最新)

    此外,面试者需要了解HashMap和ConcurrentHashMap的内部实现。 六、设计模式 设计模式是解决软件开发中常见问题的模板,Java面试中常考单例、工厂、观察者、装饰者、代理等设计模式。理解每种模式的意图、优缺点和...

    50个有深度的java编程题目

    4. **集合框架**:熟悉ArrayList、LinkedList、HashMap、HashSet等集合类的使用,了解它们的底层实现和性能特点。理解List、Set、Map接口及其区别。 5. **多线程**:理解线程的创建方式(Thread类和Runnable接口)...

    Redis、Spring、RabbitMQ、Java线程、Mybatis、HashMap、JVM、MySQL相关问题的实例代码

    然而,HashMap不是线程安全的,如果你需要在多线程环境下使用,可以考虑使用`ConcurrentHashMap`。 JVM(Java虚拟机)是Java程序运行的平台,负责字节码的解释执行和垃圾回收。理解JVM内存模型(堆、栈、方法区等)...

    Java 面试中的陷阱

    - 知道ArrayList和LinkedList的区别,HashMap和ConcurrentHashMap的工作原理。 - 探讨泛型、迭代器、并发容器(如ConcurrentHashMap、CopyOnWriteArrayList)的使用。 7. **反射与注解**: - 理解反射的概念,...

    shouhu 面试 java 比较全的题目.docx

    - **HashMap和ConcurrentHashMap**:HashMap线程不安全的原因,以及ConcurrentHashMap如何实现线程安全。 4. **多线程** - **线程同步**:synchronized关键字的用法,以及它如何确保线程安全。 - **死锁**:解释...

    字节跳动面经1

    - HashMap与ConcurrentHashMap:面试官询问了这两种数据结构的区别,尤其是并发性能和线程安全方面。HashMap是非线程安全的,而ConcurrentHashMap是为并发设计的,提供了更好的性能。 - Vector与List:讨论了这两...

    java面试题目 java面试最常问问题 java面试题集

    - Map接口:HashMap、ConcurrentHashMap、TreeMap的特性和使用场景。 - 泛型:泛型的约束、通配符、类型擦除等概念。 5. **异常处理**: - 异常分类:如何处理Checked和Unchecked异常。 - 自定义异常:何时以及...

    2017年Java面试题,Java开发

    - HashTable, HashMap, ConcurrentHashMap是Map的实现,HashTable是线程安全的,而HashMap不是,ConcurrentHashMap提供了更好的并发性能。 - equals()和hashCode()用于定义对象的内容相等性和哈希码计算,通常在...

    Java 性能分析

    总结,Java性能分析涉及源码的深度理解、性能工具的使用以及调优策略的实施。通过这些方法,开发者可以有效地提升Java应用的运行效率,确保系统在各种条件下的稳定性和高性能。在实际工作中,结合标签中提到的“工具...

    一套java面试题,收录很多经典题目,附答案

    List、Set、Queue、Map接口及其实现类的特性和使用场景,如ArrayList与LinkedList的区别,HashMap与ConcurrentHashMap在并发环境下的表现,以及HashSet的无序性等,都是面试官喜欢考察的点。 再者,Java IO和NIO...

    毕业季分享-美团面试经验

    同时,熟悉集合框架,如List、Set、Map的实现原理和使用场景,以及它们之间的区别,例如ArrayList与LinkedList、HashMap与ConcurrentHashMap等。 其次,数据结构与算法是面试的重头戏。链表、栈、队列、堆、树、图...

    Java面试题集合部分.docx

    JDK1.7的`ConcurrentHashMap`采用锁分段技术,每个段(Segment)是一个独立的HashMap,JDK1.8则取消了Segment,直接使用CAS操作和分段锁来实现并发安全。 这些知识点涵盖了Java集合框架中的基础概念,以及HashMap在...

Global site tag (gtag.js) - Google Analytics