一.HashMap的底层实现
(1)jdk1.8之前,是采用的数组(Entry)+链表实现的,数组的初始值为16,数组储存的是链表的头结点,而hashcode算法是通过位运算来实现散列运算的
存在问题:可能多个值都计算后连接在一个链表上,链表会变得很长,查询性能低
(2)jdk1.8之后,采用的是数组(Node)+链表/红黑树实现的,当链表长度等于8时,就会将链表转换为红黑树,这样进行查询就提高了性能
二、HashMap的方法源码解析(出自java.util包)
(1)数组Node(java8后从Entry改成了Node)是由hash值,键值对kv,和指向下一个node的指针构成,hash值相同的键值对则以链表的形式进行存储
(2)当链表长度>8时,链表要转换成红黑树,当链表长度因为减少<6时,会从红黑树转换成链表
(3)由HashMap的构造函数和put函数可得,HashMap是基于懒汉模式,在首次使用的时候才初始化;
构造函数:只判断了容量的逻辑,赋特殊变量的初值
put方法:当table为空时,就调用resize函数,进行重新初始化(resize函数的作用是初始化和扩容)
(4)hash方法(位运算)
先将key的hashcode值算出来,然后再右移16位跟原先的值再异或,这样是目前使散列更均匀的方法,然后再通过位与的操作计算数组下标
1.根据k通过hashcode函数计算出hashcode值;
2.然后再讲hashcode值右移16位后再跟原来的hashcode值异或算出hash值
3.然后根据hash值和HashMap的大小-1(n-1)进行异或得到最后数组的下标(因为Hashmap的大小总是2的n次方)
(5)扩容resize()方法
当数组的容量已经用了75%以上(扩容因子),就会重新扩容,创建一个原来两倍的数组,将原来的数组复制进去
三、HashTable的底层实现与源码
HashTable对于public方法都加了synchronized修饰符,会获取当前方法调用者的锁,所以是线程安全的
(1)构造函数会初始化
(2)hash方法就是算出来的hashcode值跟2^16-1位运算后除数组长度
(3)扩容也是达到75%,扩容两倍
(4)一直是数组和链表的实现
四、ConcurrentHashMap的底层实现
(1)jdk1.7时,ConcurrentHashMap是由Segment数组和多个HashEntry数组组成,Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面的提到的锁分离技术,而每一个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构一样 。
Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap中扮演锁的角色,对HashEntry数组进行修改时,必须首先获得与它对应的Segment锁。
static class Segment<K,V> extends ReentrantLock implements Serializable {
当它执行put操作时,会进行第一次key的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS操作进行赋值,然后进行第二次hash操作,找到相应的HashEntry的位置,这里会利用继承过来的锁的特性,在将数据插入指定的HashEntry位置时(链表的尾端),会通过继承ReentrantLock的tryLock()方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用tryLock()方法去获取锁,超过指定次数就挂起,等待唤醒。
ps:可重入锁ReentrantLock之后会跟所有的锁统一进行讲解
(2)jdk1.8之后,直接用Node数组+链表/红黑树的数据结构来实现,并发控制使用Synchronized和CAS来对Node数组中的链表头节点操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本。
五、ConcurrentHashMap的方法源码解析(jdk1.8之后)
相比于HashMap出自于java.util包,ConcurrentHashMap出自java.util.concurrent(JUC)包
(1)put(putval)函数
由于之前构造函数仍然没有初始化,在初次使用时才初始化
其中当发生hash碰撞时,synchronized会锁住头节点,去遍历是否存在,存在就更新,不存在就在尾部添加。
具体步骤:
1.如果没有初始化就先调用initTable()方法来进行初始化过程
2.如果没有hash冲突就直接CAS插入
3.如果还在进行扩容操作就先进行扩容
4.如果存在hash冲突,就加锁(synchronized)来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入
5.最后一个如果该链表的数量大于阈值8,就要先转换成黑红树的结构,break再一次进入循环
6.如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容
(2)hash函数,跟HashMap相同,将hashcode码右移16位再跟原来异或运算,在跟2^16-1位运算算出hash值
int hash = spread(key.hashCode());
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
六、ConcurrentHashMap在jdk1.7和1.8的区别总结
jdk1.8里的锁比1.7中的segment分的更细,只要不出现hash冲突,就不会出现并发和锁的情况
(1)首先使用无锁原子操作CAS插入头节点,失败则循环重试
(2)若头节点已经存在,则尝试获取头节点的同步锁,再进行操作
七、手写实现一个简单的HashMap
先分析一下要写的类、接口、方法和变量有:
Map接口,Entry接口,自己定义的HashMap实现类,其中包括Entry实现类,包括put方法,get方法,hash方法,getIndex方法,扩容方法和一些初始的常量等
(1)BaseMap接口,定义put和get方法
public interface BaseMap<K,V> {
public V put(K k,V v);//放入值
public V get(K k);//获取值
}
(2)BaseEntry接口,定义了getKey和getValues方法
public interface BaseEntry<K,V> {
public K getKey();//获取键
public V getValues();//获取值
}
(3)MyHashMap实现类,实现了Map接口,定义了很多常量和方法
public class MyHashMap<K,V> implements BaseMap<K,V>{
(4)Entry实现类,定义在MyHashMap类中,定义了常量和构造函数及实现方法
static class Entry<K,V> implements BaseEntry<K,V>{
//定义变量和常量
K k;
V v;
Entry<K,V> next;//Entry链表指针
//构造函数
public Entry(K k,V v,Entry<K,V> next){
this.k=k;
this.v=v;
this.next=next;
}
//实现接口的抽象方法
@Override
public K getKey() {
return k;
}
@Override
public V getValues() {
return v;
}
}
(5)定义一些Map和Entry的常量
//定义HashMap类的常量
private int defaultLength = 16;//默认长度
private double defaultAddFactor = 0.75;//默认负载因子
private double useSize;//使用数组位置的数量
private Entry<K, V>[] table;//数组
(6)HashMap的构造函数,在构造函数中复制和初始化
//HashMap的构造函数
public MyHashMap(){
this(16,0.75);
}
public MyHashMap(int defaultLength,double defaultAddFactor){
if(defaultAddFactor<0){
throw new IllegalArgumentException("数组初始大小异常");
}
if(defaultAddFactor<=0||Double.isNaN(defaultAddFactor)){
throw new IllegalArgumentException("因子设置异常");
}
this.defaultLength=defaultLength;
this.defaultAddFactor=defaultAddFactor;
table = new Entry[defaultLength];//初始化了HashMap
}
(7)hash和计算数组下标方法,数组下标等于hashcode右移16位异或hashcode后,再跟length-1位运算
public int hash(Object key){
int h;
return (key==null)?0:(h=key.hashCode())^(h>>>16);
}
//计算数组下标
public int getIndex(int hash,int length){
return (length-1)&hash;
}
(8)Put方法,判断是否为空,为空就新建,不为空就看能否找到节点,找到就更新,找不到就添加在链表最后
public V put(K k, V v) {
if(useSize>defaultLength*defaultAddFactor){
resize();//扩容
}
int index = getIndex(hash(k),table.length);
Entry<K,V> entry = table[index];
Entry<K,V> newEntry = new Entry<>(k,v,null);
if(entry==null){
table[index] = newEntry;
useSize++;
}else{
Entry<K, V> t = entry;
if (t.getKey() == k || (t.getKey() != null && t.getKey().equals(k))) {//相同key 对应修改当前value
t.v = v;
} else {
while (t.next != null) {
if (t.next.getKey() == k || (t.next.getKey() != null && t.next.getKey().equals(k))) {//相同key 对应修改当前value
t.next.v = v;
break;
} else {
t = t.next;
}
}
if (t.next == null) {
t.next = newEntry;
}
}
}
return newEntry.getValues();
}
(9)get方法,判断entry是否为空,为空就报错,不为空就依次向下遍历
public V get(K k) {
int index = getIndex(hash(k),table.length);
Entry<K,V> entry = table[index];
if(entry==null){
throw new NullPointerException();
}
while(entry!=null){
if(k==entry.getKey()||k.equals(entry.getKey())){//相等直接返回
return entry.v;
}else{//向下遍历
entry=entry.next;
}
}
return null;
}
(10)resize扩容方法,先用list将原来存着,然后新建一个,对Map的常量进行改变,然后调用put进行重新插入
public void resize(){
Entry<K,V>[] newTable = new Entry[defaultLength*2];
List<Entry<K,V>> list = new ArrayList<>();
for (int i = 0; i <table.length ; i++) {//创建新数组,将原数组拷贝进list中
if(table[i]==null){
continue;
}
Entry<K,V> entry =table[i];
while(entry!=null){
list.add(entry);
entry=entry.next;
}
}
if(list.size()>0){//重置参数,调用put方法把原来的放入新数组中
useSize=0;
defaultLength=defaultLength*2;
table=newTable;
for (Entry<K,V> entry:list) {
if(entry.next!=null){
entry.next=null;
}
put(entry.getKey(),entry.getValues());
}
}
}