三、Java并发-HashMap,HashTable和ConcurrentHashMap的源码学习

本文详细解析了HashMap和ConcurrentHashMap的底层实现原理,包括JDK1.8前后的变化,如数组+链表/红黑树的结构,锁分离技术,以及手写实现一个简单的HashMap。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一.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());
            }
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值