转载请注明出处:http://blog.csdn.net/jevonsCSDN/article/details/54619114 【Jevons’Blog】
以前只知道HashMap是线程不安全的,拿来就用,也不会考虑会出现什么后果,直到最近在学习中终于暴露出了HashMap的短板出来,可又百思不得其解,于是在网上拜读了若干大牛有关HashMap的分析文章,发现他们其实写于很早之前,而HashMap的源码都已作更新,所以干脆抽空对HashMap的新版源码从头到尾地梳理了一遍,并写一篇分析博文帮助学习。 HashMap可以说是Java中最常用的集合类框架之一,是Java语言中非常典型的数据结构,我们总会在不经意间用到它,很大程度上方便了我们日常开发,因此我们更需要去把控好它的脉络。本文基于Java7的源码做剖析,内容有点长,在浏览的时候建议通过目录定位,文章若有不正之处欢迎指出。
好了开刀吧,在进入HashMap的世界之前,我们先来了解一下它的家庭成员:
成员变量
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16,默认初始容量为16,必须为2的幂;
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量值,容量值必须为2的幂且小于该值;
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认加载因子
/**
* An empty table instance to share when the table is not inflated.
*/
static final Entry<?,?>[] EMPTY_TABLE = {};//空的Entry数组,未调整表容量前共享。
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;//必须重设容量的Entry数组表,长度必须为2的幂;
/**
* The number of key-value mappings contained in this map.
*/
transient int size;//HashMap的大小,即Entry元素总量;
/**
* The next size value at which to resize (capacity * load factor).
* @serial
*/
// If table == EMPTY_TABLE then this is the initial capacity at which the
// table will be created when inflated.
int threshold;//临界值,如果表是空的,则该值作为空表膨胀的初始容量;
/**
* The load factor for the hash table.
*
* @serial
*/
final float loadFactor;//哈希表的加载因子
/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
transient int modCount;//hashMap结构修改次数统计
/**
* The default threshold of map capacity above which alternative hashing is
* used for String keys. Alternative hashing reduces the incidence of
* collisions due to weak hash code calculation for String keys.
* <p/>
* This value may be overridden by defining the system property
* {@code jdk.map.althashing.threshold}. A property value of {@code 1}
* forces alternative hashing to be used at all times whereas
* {@code -1} value ensures that alternative hashing is never used.
*/
// 默认备用哈希算法启用阈值,默认大小为Integer.MAX_VALUE,该变量被静态内部类Holder引用。
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
/**
* A randomizing value associated with this instance that is applied to
* hash code of keys to make hash collisions harder to find. If 0 then
* alternative hashing is disabled.
*/
//哈希种子,用于降低key的hash碰撞概率,如果为0则禁用备用哈希算法;
transient int hashSeed = 0;
静态内部类Holder的源码:
/**
* holds values which can't be initialized until after VM is booted.
* 控制一些数据在VM启动之前不能初始化
*/
private static class Holder {
/**
* Table capacity above which to switch to use alternative hashing.当表容量溢出时使用备用哈希算法。
*/
static final int ALTERNATIVE_HASHING_THRESHOLD;
static {
//获取系统变量jdk.map.althashing.threshold,获取备用哈希算法阈值,默认为-1
String altThreshold = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction(
"jdk.map.althashing.threshold"));
int threshold;
try {
//初始化阈值
threshold = (null != altThreshold)
? Integer.parseInt(altThreshold)
: ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;
// disable alternative hashing if -1
//如果阈值为-1,则禁用备用哈希算法
if (threshold == -1) {
threshold = Integer.MAX_VALUE;
}
if (threshold < 0) {
throw new IllegalArgumentException("value must be positive integer.");
}
} catch(IllegalArgumentException failed) {
throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
}
//初始化备用哈希算法阈值
ALTERNATIVE_HASHING_THRESHOLD = threshold;
}
}
为了理解Holder这个静态内部类,可真是在翻了N久的资料,很多文章讲到这里都是直接跳过,本人也是看得云里雾里,怎么莫名其妙的蹦出这么个东西,好像在源码中也没多大用处,没错,它是没多大用,至少对于目前的我们这种菜鸡来说,因为它涉及到了一种JDK1.7新加入的哈希算法:sun.misc.Hashing.stringHash32((String) k)
,针对String类型的key,提供一个新的hash算法处理hashcode分布以减少冲突,这个算法是不稳定的,还在实验阶段,默认情况下是关闭的,要想启用这个新特性,需要手动设置jdk.map.althashing.threshold
为非负数(默认为-1),这一点可以从Holder源码中看出。
下面引用Mikhail Vorontsov在关于Changes to String internal representation made in Java 1.7.0_06一文中的几段话作解释:
There is another change introduced to String class in the same update: a new hashing algorithm. Oracle suggests that a new algorithm gives a better distribution of hash codes, which should improve performance of several hash-based collections: HashMap, Hashtable, HashSet, LinkedHashMap, LinkedHashSet,WeakHashMap and ConcurrentHashMap. Unlike changes from the first part of this article, these changes are experimental and turned off by default.
这有另一个关于String类更新的介绍:一个新的哈希算法。Oracle声称这个新的哈希算法会提供更好的哈希码分布,这将会改善一些基于哈希码集合的性能:HashMap,Hashtable,HashSet,LinkedHashMap,LinkedHashSet,WeakHashMap和ConcurrentHashMap。不像文章开头所说的那个改变,这些改变是实验性的,默认是关闭的。
As you may guess, these changes are only for String keys. If you want to turn them on, you’ll have to set a jdk.map.althashing.threshold system property to a non-negative value (it is equal to -1 by default). This value will be a collection size threshold, after which a new hashing method will be used. A small remark here: hashing method will be changed on rehashing only (when there is no more free space). So, if a collection was rehashed last time at size = 160 and jdk.map.althashing.threshold = 200, then a method will only be changed when your collection will grow to size of 320 (approximately).
正如你所想那样,这些新特性只用于String类型的Key。如果你想启用这个特性,你可以将系统参数
jdk.map.althashing.threshold
设置为非负数(默认为-1),这个值将会成为集合大小的阈值,新的哈希算法将会在超越阈值时使用。提醒一下:哈希算法的只会在重算hash时改变(当没有多余空间的时候)。所以,如果一个集合上一次rehash时的大小为160,而jdk.map.althashing.threshold = 200
,则新的哈希算法将会在集合大小到达320(大概)时启用。
是不是已经有点感觉了?新的hash算法的使用只有在rehash中才会用到,而这个Holder静态内部类,只是加载并初始化ALTERNATIVE_HASHING_THRESHOLD
参数而已。有兴趣的话可以仔细看一看这篇文章,另外在Stark Overflow里面也有相关问答。如果还搞不懂,可以先放下以后再看,你只需知道一般情况下,我们不会用到它就是了,要是非要弄个一清二白,非常建议你重复一下我的求索过程,茫茫net中求知去吧~
构造方法:
Constructor and Description |
---|
HashMap() Constructs an empty HashMap with the default initial capacity (16) and the default load factor (0.75). 构造一个空的HashMap,默认初始容量为16,默认加载因子为0.75。 |
HashMap(int initialCapacity) Constructs an empty HashMap with the specified initial capacity and the default load factor (0.75).构造一个空的HashMap,指定初始容量,默认加载因子为0.75。 |
HashMap(int initialCapacity, float loadFactor) Constructs an empty HashMap with the specified initial capacity and load factor.构造一个空的HashMap,指定初始容量和加载因子。 |
HashMap(Map<? extends K,? extends V> m) Constructs a new HashMap with the same mappings as the specified Map.构造一个映射关系与指定 Map 相同的 HashMap。 |
在这四个构造方法中,其他三个构造方法都共同调用了第三个构造方法:
//其他三种构造方法最后都指向了该构造方法
public HashMap(int initialCapacity, float loadFactor) {
//检查初始容量是否小于0,是则抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//检查初始容量是否大于默认最大容量值,是则重置为MAXIMUM_CAPACITY
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//检查加载因子是否合法
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//指定加载因子
this.loadFactor = loadFactor;
//初始化阈值
threshold = initialCapacity;
//初始化函数,里面是空的,供子类调用
init();
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
inflateTable(threshold);
putAllForCreate(m);
}
下面开始分析HashMap的几个常用方法的源码:
put方法
public V put(K key, V value) {
//检查是否为空表,是则膨胀容量
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//检查key是否为null,这个很熟悉吧
if (key == null)
return putForNullKey(value);
//计算key的hash值
int hash = hash(key);
//获取bucketIndex,即在table中存放的位置
int i = indexFor(hash, table.length);
//取出该索引下的Entry,遍历单链
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//检查hash码是否相同,key是否相等
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
//该key已存在,取出对应的value并转移
V oldValue = e.value;
//存入新的value
e.value = value;
//该方法内容为空,供子类重写所用
e.recordAccess(this);
//返回对应的旧value
return oldValue;
}
}
//记录表结构修改次数;到了这里证明,该table中并不存在该key,向表中增加Entry
modCount++;
//增加Entry
addEntry(hash, key, value, i);
//返回空值
return null;
}
从源码中我们可以看到,put方法进行了如下操作:
1. HashMap是在put操作的时候才开始膨胀的;
2. 然后判断输入的key是否为空值,如果为空则调用putForNullKey(V)设入空key(原理差不多,但需要注意,空Key都是放在table[0]里面的);
3. hash(key)获取哈希码;
4. indexFor(hash, table.length)获取存放位置的索引;
5. 遍历table[i],检查是否存在,存在则覆盖并返回旧值;
6. 不存在,准备修改表结构,先记录次数;
7. 调用addEntry(hash, key, value, i)增加元素。
这里面涉及到几个函数,我们依次分析就明白了。
inflateTable :
/**
* Inflates the table.
* 膨胀表容量
*/
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
//将指定的表容量toSize传入,获取大于或等于toSize的2的幂值
int capacity = roundUpToPowerOf2(toSize);
//获取下一次膨胀的阈值;
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//创建指定容量的新表
table = new Entry[capacity];
//初始化哈希种子作为备用
initHashSeedAsNeeded(capacity);
}
为了保证表容量为2的幂,必须将当初初始化threshold
时指定的initialCapacity
过滤一遍,那为什么一定要保证容量为2的幂呢?那就是资源浪费和效率的二选一了,而显然JDK开发人员选择了后者,后文分析到相关函数时再作介绍。下面是roundUpToPowerOf2(toSize)
的源码:
roundUpToPowerOf2 :
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
int rounded = number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (rounded = Integer.highestOneBit(number)) != 0
? (Integer.bitCount(number) > 1) ? rounded << 1 : rounded
: 1;
return rounded;
}
先来理一下思路:
- 判断number是否大于
MAXIMUM_CAPACITY
,是则返回MAXIMUM_CAPACITY
,否则进入第二步; - 获取nubmer中的1出现的最高位(待会细讲)赋给rounded,若rounded等于零,返回1,否则进入第三步;
- 获取number的1位出现的次数,若大于1,则rounded左移一位 (保证为2的幂),否则rounded为1,返回rounded;
因此不管结果如何,最后该函数返回的都是2的幂值。下面介绍第二步和第三步涉及到的Integer相关函数。
Integer
(已经了解了?直接跳过。)
highestOneBit (int)
//该函数实现获取指定int数的二进制数中1出现的最高位
public static int highestOneBit(int i) {
// HD, Figure 3-1
i |= (i >> 1);
i |= (i >> 2);
i |= (i >> 4);
i |= (i >> 8);
i |= (i >> 16);
return i - (i >>> 1);
}
WTF?!又见位运算,高大上啊有没有!但是有没有一脸懵逼的感觉?好吧,快告诉我不是只有我才这么无聊去研究这个是怎么实现的。先来个简单的4bit运算,假设有个数 i=0110,我们来最笨的方法一位一位的移动:
有没有看明白?它其实就是通过不断的右移,再与原数i做或运算,重复以上步骤,得到一个自1的最高位到最低位都是1的数,如上面的0111,然后再拿它来和它的右移1位(无符号右移)得到的值做减运算,从而得到我们最终想要的结果:自1的最高位之后的所有低位都是0的数,如上面的0100。而我们的int的长度始终都是4个字节,也就是32bit,所以上面要进行31位的右移操作。还有疑惑的话不妨动手试一试就明白了。
bitCount (int)
//该函数实现统计指定int数的二进制数中1出现的的次数。
public static int bitCount(int i) {
// HD, Figure 5-2
i = i - ((i >>> 1) & 0x55555555);
i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
i = (i + (i >>> 4)) & 0x0f0f0f0f;
i = i + (i >>> 8);
i = i + (i >>> 16);
return i & 0x3f;
}
这又是什么鬼?简直丧心病狂!这里面用到了“分治”思想(如果不想看的也可以直接跳过本段),要统计32位数中1出现的次数,需要逐步分组并组内求和得到对应位的数,每次分组的位数加倍,以2位一组作为起始统计:
先来分析一下第一行代码:i = i - ((i >>> 1) & 0x55555555);
假设有个数 0xBC637EFF:1011 1100 0110 0011 0111 1110 1111 1111
进行第一次分组运算,每2位一组:
可以看出已经达到了我们想要的效果,那开发人员到底是怎么想到的呢?我也不知道[尴尬],在这里我就说说我的理解吧。请结合上图理解下面分析
- (i >>> 1)先将i无符号右移,则每2位中的高位移向低位,我们的目的是在这基础上再将每2位中的高位置0(此时的高位为原每2位中的低位);
- (i >>> 1)& 0x55555555:将每2位中的高位置0;
此时将出现以下结果:
1011 1100 0110 0011 0111 1110 1111 1111 [i]
0101 0100 0001 0001 0001 0101 0101 0101 [(i>>1)&0x55555555]
对比之下不难发现,上一行减下一行刚好是原数中每2位中1出现的次数。我们拿出最高的2位出来比较就很明显了:
10
01 :此数的高位永远为0,而低位则是上一行的高位,上下两数之差必等于上一行中1出现的次数。
这其实等价于i = (i& 0x55555555) + ((i >>> 1) & 0x55555555),这样更好理解,把原i和0x55555555相与过滤掉每2位中的高位,这样就只剩下低位了,而(i >>> 1) & 0x55555555又把高位移到了低位,两个数相加同样等于1出现的次数。理解了这个,后面就不难理解了吧,原理都是一样的。
下面了分析inflateTable(int)
函数里面涉及到的第二个函数:
initHashSeedAsNeeded
/**
* Initialize the hashing mask value. We defer initialization until we
* really need it.
* 初始化哈希掩码值。我们延迟初始化它直到我们需要它的时候。
*/
final boolean initHashSeedAsNeeded(int capacity) {
//检查当前备用哈希算法状态,hashSeed初始值为0
boolean currentAltHashing = hashSeed != 0;
//检查是否需要启用备用哈希算法
//一般情况下,capacity小于Holder.ALTERNATIVE_HASHING_THRESHOLD,因此该值为false
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
//进行异或判断,一般情况下为switching为false
boolean switching = currentAltHashing ^ useAltHashing;
//若switching=true,则进行以下操作
if (switching) {
//若useAltHashing=true,返回随机hashSeed,否则返回0;
hashSeed = useAltHashing
? sun.misc.Hashing.randomHashSeed(this)
: 0;
}
return switching;
}
这个方法用于决定是否启用新的hash算法,他被两个方法所调用:
- inflateTable(int toSize)
- resize(int newCapacity)
hash
final int hash(Object k) {
int h = hashSeed;
//检测hash种子的状态,决定是否启用新的hash算法。
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
//使用旧的哈希算法
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
//保证hashCode 不同的算法,看不懂就随缘啦,太凶残了
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
indexFor
/**
* Returns index for hash code h.
* 返回该hashcode在table中对应的索引
*/
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";保证表容量必须为2的幂。
//hashcode在table中对应的索引
return h & (length-1);
}
这里就是需要保证容量必须为2的幂的原因,因为length为2的幂的话,length-1刚好就是索引范围:[0,length),形成左闭右开区间,而又恰巧每一个有效位都为1,例如:
Capacity=16
则length=16, 二进制为0000 0000 0000 0000 0000 0000 0001 0000
lenght-1 =15,二进制为0000 0000 0000 0000 0000 0000 0000 1111
那么通过h & (length-1)
得到的就是key在表中的索引位置。h & (length-1)
与h%length
等价不等效,位运算的速度和效率是非常高的,这就是容量必须为2的幂的原因。
接下来就是遍历Entry单链了,这个应该很好理解,Entry是以单链的形式存在的,用于解决hash碰撞时的存放问题。最后就是addEntry(),向表中插入元素,内容拉的有点长,可以点下锚点跳至put源码整理一下思路,现在再去看应该一目了然了吧。接下来基本上没什么难度了,读懂源码的表面意思就ok。
addEntry
void addEntry(int hash, K key, V value, int bucketIndex) {
//检查存放元素的数量是否大于或等于阈值,该bucketIndex下的表位置是否不为空
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容至原来2倍
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
//重新计算索引
bucketIndex = indexFor(hash, table.length);
}
//容量充足,进入创建Entry操作
createEntry(hash, key, value, bucketIndex);
}
resize
(或者先看createEntry方法?)
//重新调整表容量
void resize(int newCapacity) {
//备份表数据
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//检查旧表的容量是否已是最大值,是则终止扩容直接返回
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//创建空的新表
Entry[] newTable = new Entry[newCapacity];
//转移表数据,第二个参数决定是否重算hash码
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//新表覆盖旧表
table = newTable;
//计算下一次调整的阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
transfer
/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//遍历table中的Entry
for (Entry<K,V> e : table) {
//遍历Entry单链
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//重新计算索引
int i = indexFor(e.hash, newCapacity);
//置空e.next。将table[i]的空引用赋值给e.next,此时Entry链表中只有一个e。
//也就是这里,会触发多线程并发问题
e.next = newTable[i];
//将e放入新table[i]中;
newTable[i] = e;
//将next链表赋值给e,继续循环遍历。
e = next;
}
}
}
这里的后半部分可能比较难以理解,其实就是先把Entry从一个拖家带口的家庭里抽出来,单独放到新的table中的过程,目的就是想让表中的元素尽量单独存在于表中,而不是以多个单链的形式存在,从而提高HashMap的性能。画了个图助于理解,丑了点,凑合看吧。。。
多线程并发问题
那么这就牵扯到了多线程并发问题了,我在源码注释中也提到,
e.next =
,就是问题所在,这里将该索引下的Entry元素单链处理成单个元素,那么链表之后的元素就是null
newTable[i]
了,而恰巧你在此刻又进行了get操作,又很恰巧你的Entry元素在被处理掉的链表中,那么他get到的还是原table中的数据,自然也就拿不到数据了,就会报空指针异常。最后一句e
也是,假如你get的元素恰巧是之前这个e,而此刻e又被next顶掉了,同样也会报空指针异常。
= next
createEntry
(跳回resize源码)
void createEntry(int hash, K key, V value, int bucketIndex) {
//初始化索引为bucketIndex的表位置
Entry<K,V> e = table[bucketIndex];
//初始化Entry,可能会引发多线程并发问题
table[bucketIndex] = new Entry<>(hash, key, value, e);
//元素加1
size++;
}
多线程并发问题
Entry是一个链表结构,如果在
new Entry<>(hash, key, value, e)
操作中,有两个线程同时在此刻拿到相同的e,那么这两个线程就会竞争作为e的链头的所有权,势必会有一个会被覆盖掉,而在你进行get操作想取被覆盖掉的entry,那自然也是取不到的,返回空值。
了解一下Entry的内部结构:
Entry
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
//体现了entry的链表特性
Entry<K,V> next;
int hash;
/**
* Creates new entry.
* 将新new的entry插入到旧entry的链头
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
//省略展示部分方法
}
好了,到这里我们put方法所涉及到的所有操作都分析完了。下面来分析get方法。
get
public V get(Object key) {
//检测是否为空key
if (key == null)
return getForNullKey();
//获取相应的Entry
Entry<K,V> entry = getEntry(key);
//检查entry是否为空,是则返回null;否则返回对应的value
return null == entry ? null : entry.getValue();
}
getEntry
final Entry<K,V> getEntry(Object key) {
//检查表中元素数量
if (size == 0) {
return null;
}
//检测key是否为空,是则返回0;否则返回key的hash码
int hash = (key == null) ? 0 : hash(key);
//根据hash码和表长度获取索引,从table中取出entry
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
//检测hash是否相同,key的内存地址是否相等,key是否为null,key的equals方法返回值是否为true(之所以要比较这个是因为可以通过重写equals实现两个不同内存地址的对象返回true值)。
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//返回entry
return e;
}
return null;
}
总结
本文重在梳理HashMap内部实现原理,至于HashMap的多线程问题,可以通过以下方式解决:
- 在包含HashMap的方法中实现同步机制,效率太低
- 外部包装:
Map<K,V> map = Collections.synchronizedMap(new HashMap<K,V>());
- HashTable,效率太低
- 使用JDK1.5中引进的
Concurrent
包下的ConcurrentHashMap
,相对安全高效,建议使用。我在另一篇文章中也有介绍。
写在最后
到这里HashMap的一些常用方法源码就分析完了,其中也提到了有关可能引发多线程并发问题的所在,摸清了这个数据结构,以后用起来也就胸有成竹了,当然,有兴趣的同学也可以尝试去写自己的Map结构,在这里就不再赘述了。相信如果已经理解了上面的内容,那么阅读HashMap的其他源码并不是什么难事,加油吧少年!