HashMap
视频地址
哈希表的O(1)的平均查找、插入、删除时间
一、Java7 HashMap
初始容量必须是2的指数次幂
默认初始容量:1<<4 = 16
左移一位表示乘2,左移4位表示乘以2的4次方。
默认的加载因子是0.75。
HashMap初始化数组不是在构造函数中创建数组的,而是在第一次调用put函数的时候。
如果初始化的时候大小不是2的幂,将向上调整成2的幂。
1、put函数(没初始化的话,会在put函数中进行初始化)
public V put(K key, V value)
{
if (table == EMPTY_TABLE)
{
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next)
{
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
{
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
每个元素的hashCode是从-2^ 31 到 + 2^31,可能是这些数字,如果取模放到数组中,有两个缺点:
- 负数取余是负数,所以要把负数先变成正数
- 速度较慢
hash函数(使得散列均匀)
final int hash(Object k)
{
int h = hashSeed;
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).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
hash函数可以保证分布均匀一点。
indexFor函数源码(结果拿到数组索引(按位与得到))
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
2、为什么数组长度一定要是2的指数次幂(为了方便按位与得到索引)
求出hash函数求出hash值之后,源码中不是用的取模,而是使用hash值和数组长度减1进行按位与,hash&(length-1),因为length是2的次幂,所以length-1最后应该是都是1,比如15的二进制是0000 1111,所以得到的是前面都是0,后面都是1,这样和一个数进行按位与,前面还都是0,后面可能有0有1,但是一定小于等于length-1。
3、放入元素时执行的操作
如果hash值一样,key也一样,就会把值替换,返回,否则addEntry(超过容量会扩容size*加载因子,对所有元素重新计算hash)
在resize函数中进行重新计算hash值,放到新的位置。
扩容的容量会变成以前两倍。
4、链表节点Entry
HashMap底层好像就是用的这个数组
static class Entry<K,V> implements Map.Entry<K,V>
{
final K key;
V value;
Entry<K,V> next;
int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
5、Java7的HashMap的问题
容易碰到死锁(多线程会碰到)
隐患(可能退化成链表,Hahs值都相同,如String类)
二、Java8 HashMap
1、Java8 HashMap的改进
链表变成树的阈值为8。
红黑树的排序主要基于hashCode,如果两个元素的HashCode相同,且两个元素类相同和都实现了Comparable接口,则compareTo方法就会被用来排序。
TreeNode节点是普通节点(Entry?)的两倍大,所以在链表里面有足够多的元素的时候才使用它(根据TREEIFY_THRESHOLD?),当链表很小的时候,在你扩容或者resize的时候会转变成链表。在分布均匀的时候,红黑树代替链表用的比较少。
红黑树很少被用到, 理想情况下在随机HashCode的实现中, 桶(链表)中节点的个数服从泊松分布。桶中有1个、2个、3个元素…的概率如下:
从上图可以看出,超过8的概率已经很小了。所以链表变成红黑树的阈值选择8.
put方法
public V put(K key, V value)
{
return putVal(hash(key), key, value, false, true);
}
hash函数(和Java7不同)
static final int hash(Object key)
{
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); //异或,就是不进位的加法
}
putVal函数
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict)
{
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) //如果数组当前位置还没有元素
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))//如果hash相等,key相等,直接把值覆盖掉。
e = p;
else if (p instanceof TreeNode) //如果节点是TreeNode则执行树节点插入操作
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else
{//链表
for (int binCount = 0; ; ++binCount)
{
if ((e = p.next) == null)
{
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st,如果到了链表变成树的阈值,则变成红黑树
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
resize函数
final Node<K,V>[] resize()
{
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0)
{
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
Java8 HashMap不用rehash
e.hash & oldCap== 0
oldCap是2的指数次幂,所以只有最高位是1,这样就可以保证最高位按位与之后是0或者1,其他都是0,那么rehash之后要么还在原来位置,要么再加上oldCap。
JDK7resize中的transfer函数
void transfer(Entry[] newTable, boolean rehash)
{
int newCapacity = newTable.length;
for (Entry<K,V> e : table)
{
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 = newTable[i];
newTable[i] = e;
e = next;
}
}
}
源码中可能设计到的知识点
红黑树特性
- 每个节点要么是红的要么是黑的
- 根节点必须是黑色的
- 每个叶子节点(指的是尾部NIL指针或者NULL节点 )都是黑的
- 如果一个节点上红的,那么他的两个儿子都是黑的
- 对任意节点而言,其到叶节点树尾端NIL指针的每条路径都包含相同的黑节点
插入6之后就会打破平衡,因为一个高度为2,一个为4,所以要旋转保持平衡。
原码
最高位为符号位,0代表正数,1代表负数,非符号位为该数字绝对值的二进制表示。
如:
127的原码为0111 1111
-127的原码为1111 1111
反码
正数的反码与原码一致;
负数的反码是对原码按位取反,只是最高位(符号位)不变。
如:
127的反码为0111 1111
-127的反码为1000 0000
补码
正数的补码与原码一致;
负数的补码是该数的反码加1。
如:
127的补码为0111 1111
-127的补码为1000 0001
ConcurrentHashMap
Segment (段)= 小HashMap
每段自带一把锁
- 根据key算出对应的Segment是哪个
- Segment.put
Java7 ConcurrentHashMap
1、Segment构造函数
会传进来HashEntry数组,因为一个Segment包含一个HashEntry数组。
Segment(float lf, int threshold, HashEntry<K,V>[] tab)
{
this.loadFactor = lf;
this.threshold = threshold;
this.table = tab;
}
1、java7 Segment put方法(和HashMap的put差不多)
final V put(K key, int hash, V value, boolean onlyIfAbsent)
{
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); //取出元素,在hashMap中是table[i],这样存在并发问题
for (HashEntry<K,V> e = first;;)
{
if (e != null)
{
K k;
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 {
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;
}
2、HashEntry定义(和HashMap中的Entry差不多)
static final class HashEntry<K,V>
{
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
为了确保读操作能够看到最新的值,将 value 设置成 volatile,这避免了加锁。
3、
默认并非级别16(也就是分几段)
最小Segment容量2
比如数组长16,有8个Segment,每个S有小的hashMap,HashMap.table.size=2;
会产生8个Segment数组,里面有HashMap即有Entry数组,大小为2。
如果分段树即并发级别(下面的ssize)传的不是2的指数次幂,将会转成2的指数次幂
ConcurrentHashMap构造函数
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel)
{
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// Find power-of-two sizes best matching arguments
int sshift = 0;
int ssize = 1; //就会变成Segment大小
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c; //将c变为整数倍,最接近原来容量,比如原来initialCapacity是23,ssize是8,可以把c变成3。
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1; //将cap变为最接近c的2的指数次幂
// create segments and segments[0]
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
上面的构造函数也就是把Segment的大小变成2的指数次幂,再把HashEntry的大小先变成Segment的倍数(向上取整),如果不是2的指数次幂,再将它转化成最近的2的指数次幂。
ConcurrentHashMap的put方法
public V put(K key, V value)
{
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask; //找出应该放在哪个Segment下面去
if ((s = (Segment<K,V>)UNSAFE.getObject(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j); //创建Segment
return s.put(key, hash, value, false);
}
ensureSegment方法
private Segment<K,V> ensureSegment(int k)
{
final Segment<K,V>[] ss = this.segments;
long u = (k << SSHIFT) + SBASE; // raw offset
Segment<K,V> seg;
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null)
{
Segment<K,V> proto = ss[0]; // use segment 0 as prototype,Segment数组的第一个位置
int cap = proto.table.length;//一个Segment包含的容量
float lf = proto.loadFactor;
int threshold = (int)(cap * lf);
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // recheck,双重检查
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
Segment[0]存放了一些数据,比如每个Segment的HashEntry数组的长度等
初始化好之后,在Segment的put函数的时候会进行加锁。
Segment继承了ReentrantLock类。
JDK8 ConcurrentHashMap
JDK8锁的不再是一个Segment,而是HashEntry中的一个对象。
锁的链表或者树的第一个节点
Unsafe使用及CAS的使用
import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class UnsafeTest
{
private int i = 0;
private static Unsafe unsafe = null;
private static long I_OFFSET; //i属性的偏移量
static
{
try
{
// 只有bootstrap加载器加载的才能拿到Unsafe类,不然加载器是不一样的。
// unsafe = Unsafe.getUnsafe();
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
//System.out.println(unsafe);
// System.out.println(field);
I_OFFSET = unsafe.objectFieldOffset(UnsafeTest.class.getDeclaredField("i"));
//System.out.println(I_OFFSET);
}
catch (NoSuchFieldException | IllegalAccessException e)
{
e.printStackTrace();
}
}
public static void main(String[] args)
{
final UnsafeTest unsafeTest = new UnsafeTest();
new Thread(new Runnable() {
@Override
public void run()
{
while (true)
{
boolean b = unsafe.compareAndSwapInt(unsafeTest, I_OFFSET, unsafeTest.i, unsafeTest.i+1);
//unsafeTest.i++;
if(b)
{
System.out.println(Thread.currentThread().getName()+":"+unsafe.getIntVolatile(unsafeTest,I_OFFSET));
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run()
{
while (true) {
boolean b = unsafe.compareAndSwapInt(unsafeTest, I_OFFSET, unsafeTest.i, unsafeTest.i+1);
//System.out.println(b);
//unsafeTest.i++;
if(b)
{
System.out.println(Thread.currentThread().getName()+":"+unsafe.getIntVolatile(unsafeTest,I_OFFSET));
}
try
{
Thread.sleep(500);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}).start();
}
}