HashMap&ConcurrentHashMap源码探究

目录

HashMap&ConcurrentHashMap源码探究

一、JDK1.7 HashMap

1、初始化(伪初始化)   

2、put(真初始化)

二、JDK1.7 ConcurrentHashMap

0、数据结构

1、初始化(这回真的初始化了一点东西)

2、 put(实际是put了两次)

三、JDK1.8 HashMap

一、引入红黑树概念:(实现挺繁琐的,就只先看规则)

四、JDK1.8 ConcurrentHashMap


 

HashMap&ConcurrentHashMap源码探究

一、JDK1.7 HashMap

1、初始化(伪初始化)   

    // 默认table数组大小16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    // 默认加载因子 0.75
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    public HashMap(int initialCapacity, float loadFactor) {
        // 数组大小不能低于0
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        // 数组大小不能超过最大限制MAXIMUM_CAPACITY=1 << 30 2的30次方
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        // 加载因子得是一个数值并且不能小于等于0
        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);
    }

不传参数,默认table大小为16,加载因子为0.75
传table大小,可以自定义table的大小,以及加载因子

2、put(真初始化)

	public V put(K key, V value) {
		// (2.1)如果是个空数组,就真初始化
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        // (2.2)不同于hashtable,与ConcurrentHashMap  key可以是null
        if (key == null)
            return putForNullKey(value);
        // (2.3)重新计算key的hash值
        int hash = hash(key);
        // (2.4)根据hash与数组长度计算当前key的数组下标
        int i = indexFor(hash, table.length);
        // 判断当前key在table[i]的链表上是否有值
        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;
            }
        }
        // (2.9)操作次数加1
        modCount++;
        // (2.5 2.6 2.7 2.8)新增当前的Entry对象,可能导致扩容,插入方式为头插法
        addEntry(hash, key, value, i);
        return null;
    }

 如果table为空,先初始化table--inflateTable(初始化的table大小)

详细步骤如下
2.1 inflateTable

    private void inflateTable(int toSize) {
        // 计算大于当前toSize的最小2的幂次方就是数组大小
        int capacity = roundUpToPowerOf2(toSize);
        // 阈值赋值,数组大小*加载因子
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        // 创建数组
        table = new Entry[capacity];
        // 判断是否重新打乱hash
        initHashSeedAsNeeded(capacity);
    }

传入参数如果大于1,就进行2的幂次方扩充,如10扩充16,方法为先减1后位运算左移1位(乘2),然后使用integer的方法highestOneBit
如果小于等于1,就是创建大小为1的table


2.2 如果key为null

    private V putForNullKey(V value) {
    	// 遍历table[i]位置上的链表,发现有key为null就覆盖并返回原值
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        // 增加操作次数
        modCount++;
        // 头插法插入entry对象
        addEntry(0, null, value, 0);
        return null;
    }

就放入table[0]的位置
放入操作就是遍历table[0]的链表,如果发现有key为null就替换返回,没有就使用头插法table[0] = new entry(hash,key,value,table[0])


2.3 重新hash

    final int hash(Object k) {
    	// 获取打乱hash的数值,默认为0,不会导致hash被打乱
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
        // 根据打乱hash的数值计算新的hash,默认hashSeed=0,进行或运算后hashcode值不变
        h ^= k.hashCode();
        // hash算法,尽量打乱低位的散列行
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

根据key的hashcode重新hash,重新hash的原因就行因为尽可能打乱低位,否则低位的重复性可能较大
如:0000 0001
       0001 0001
       0011 0001
       0111 0001
       0101 0001 如果进行下标运算,结果位置肯定都是一样的,为啥一样看2.4


2.4 计算下标

    static int indexFor(int h, int length) {
    	// hash值与数组长度相与运算,算出当前key所在数组下标
        return h & (length-1);
    }

因为key的hashcode肯定很大,不可能直接用来做下标。
计算下标使用了key的hash与table长度减1进行 & 运算。&:都为1,才为1
假设hashcode是 0101 0101
     长度16减1      0000 1111
          结果          0000 0101 实际就是hashcode的后4位,所以取值范围肯定就是0-15,不会超出。所以2的幂次方的用处在这里也有体现。让范围内每一位都有值
这也是为啥2.3要重新hash的原因


 2.5 插入

    void addEntry(int hash, K key, V value, int bucketIndex) {
    	// 如果当前数组元素个数大于了阈值,并且当前插入的数组下标发送了hash冲突,进行扩容
        if ((size >= threshold) && (null != table[bucketIndex])) {
        	// 扩容翻倍
            resize(2 * table.length);
            // 计算扩容后的hash值
            hash = (null != key) ? hash(key) : 0;
            // 计算新的数组下标
            bucketIndex = indexFor(hash, table.length);
        }
        // 头插法插入entry对象
        createEntry(hash, key, value, bucketIndex);
    }

使用同2.2一样的遍历table[i]的链表,有相同key就替换并返回,没有key就使用头插法进行插入。table[i] = new entry(hash,key,value,table[i])


 2.6 扩容条件

jdk1.7的扩容条件有两个,1-元素个数大于table长度×负载因子,2-发生了hash冲突。也就是说默认情况下,hash不进行扩充最多可放入12+15=27个元素


2.7 扩容

    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));
        // 将hashMap中数组用新数组替换
        table = newTable;
        // 计算新的阈值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

在addEntry之前,会有2.6的扩容判断,如果判断符合,则进行原table长度*2的翻倍,
先创建翻倍大小的数组,然后使用双重循环将老数组元素挪到新数组。如果不重新hash的情况下,重新计算数组下标同之前插入一致
使用key的hash & 新数组长度-1。这样得到的结果有两种:1、和原值相同。2、等于原值+扩容的数组长度。原因如下
   hash:0110 0101 所以下标还是原值
原长是:0000 1111 & hash相与后:0000 0101
扩容后:0001 1111 & hash相与后:0000 0101
     
   hash:0101 0101 所以下标变成了原值+扩容长度
原长是:0000 1111 & hash相与后:0000 0101
扩容后:0001 1111 & hash相与后:0001 0101


2.8 扩容时打乱hash

    final boolean initHashSeedAsNeeded(int capacity) {
    	// 获取当前打乱hash的值的大小(hashSeed我就叫做打乱hash,学名叫啥我也懒得查了,知道啥意思就行)
        boolean currentAltHashing = hashSeed != 0;
        // 获取jvm中配置的参数ALTERNATIVE_HASHING_THRESHOLD 也就是jdk.map.althashing.threshold
        boolean useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        // 只要打乱hash不为0或者新数组大小超过了阈值,就重新计算打乱hash
        boolean switching = currentAltHashing ^ useAltHashing;
        if (switching) {
            hashSeed = useAltHashing
                ? sun.misc.Hashing.randomHashSeed(this)
                : 0;
        }
        return switching;
    }

有个hashSeed参数,正常情况都是0.所以扩容时都不用重新hash
hashSeed是根据新扩容长度是否大于jvm参数jdk.map.althashing.threshold进行判断赋值的。
如果大于就会随机hashSeed值,hash计算时,h ^= k.hashCode();所以新的数组下标就会变化。
然后每次扩容。hash值都会重新变化,之后的每次扩容下标都不一样了

2.9 modCount参数含义

对hashMap的操作次数累计。在一边遍历hashMap,一边对hashMap进行操作,就会导致抛出异常。防止多线程并发的一个提前异常抛出

hashmap实际就是put方法稍微复杂,get方法就简单的hash算出来key的下标,然后遍历链表就行了。就不详细写过程了

3、总结

3.1 初始大小16,加载因子0.75,阈值12,在初始化时,没有初始化数组。只是把大小,加载因子,阈值确定了。

3.2 在第一次put时,进行的初始化操作,允许key为null,key为null时,数组下标就为0。

3.3 在put时,使用头插法。hash算法是对key的hashcode重新加工,不是原有的hashcode。

3.4 有个hashSeed参数,hashSeed是通过jdk.map.althashing.threshold进行判断是否赋值,在扩容时堆hash算法进行打乱。

3.5 hashmap全程不涉及到锁,所以线程不安全,扩容时可能回导致链表的死循环,在同一位置同时头插法put,会导致覆盖等问题。

二、JDK1.7 ConcurrentHashMap

0、数据结构

说ConcurrentHashMap之前,先说下他的一个数据结构,看了数据结构就非常方便理解。(图略丑,不要介意)

不同于HashMap,就一个数组+链表的方式,ConcurrentHashMap采用了,大数组加套小数组上放链表的方式。

之所以能实现线程安全,就是因为在put时,先算出大数组的下标,然后对当前这个下标的小数组lock,就实现了线程安全。也是常说的分段加锁的原理。

大数组称之为Segment数组,小数组称为HashEntry数组

1、初始化(这回真的初始化了一点东西)

	public ConcurrentHashMap() {
	    // 默认大小。16、0.75、16
	    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
	}

    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
    	// 1.1 加载因子需要大于0,初始表大小不能小于0(可以等于),并发级别需要大于0
        if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        // 1.2 并发级别上限设置
        if (concurrencyLevel > MAX_SEGMENTS)
            concurrencyLevel = MAX_SEGMENTS;
        // 1.3 ssize就是大于并发级别的最小2的幂次方。也是Segment数组的真实大小
        // sshift计算是算循环的次数,实际值就是ssize的2进制-1后,有值的低位的个数
        int sshift = 0;
        int ssize = 1;
        while (ssize < concurrencyLevel) {
            ++sshift;
            ssize <<= 1;
        }
        // segmentShift值理解成ssize的2进制-1,无值的高位的个数,segmentMask就是Segment数组的大小-1
        this.segmentShift = 32 - sshift;
        this.segmentMask = ssize - 1;
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        // c就是表大小/并发级别 用于计算出Segment的HashEntry数组初始大小
        int c = initialCapacity / ssize;
        // 1.4 这段代码我是这没读懂啥意思,希望懂的小伙伴提点一下
        if (c * ssize < initialCapacity)
            ++c;
        // 1.5 Segment的HashEntry数组初始大小,默认最小为2.表大小/并发级别的最小的2的幂次方
        int cap = MIN_SEGMENT_TABLE_CAPACITY;
        while (cap < c)
            cap <<= 1;
        // 1.6 初始化默认的Segment对象
        Segment<K,V> s0 =
            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                             (HashEntry<K,V>[])new HashEntry[cap]);
        // 1.7 创建Segment数组
        Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
        // 1.8 使用UNSAFE包进行设置Segment[0]的对象
        UNSAFE.putOrderedObject(ss, SBASE, s0); 
        this.segments = ss;
    }

1.1 不同于HashMap,默认有三个参数(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
    initialCapacity : 表的大小,除并发级别后用于计算HashEntry数组的大小,HashEntry数组的大小最小为2
    loadFactor:加载因子
    concurrencyLevel:默认的并发级别,类似于HashMap的初始大小设置,大于自己的2的最小幂次方就是Segment数组大小。最小可为1,表示segment数组大小永远为1
  
1.2 判断并发级别concurrencyLevel是否大于MAX_SEGMENTS,如果是就赋值MAX_SEGMENTS = 1 << 16
1.3 局部变量sshift与ssize作用
  如果ssize小于设置的并发级别,就循环进行位运算左移1位,相当于乘2。一直大于concurrencyLevel为止,如果concurrencyLevel=15,则ssize=16
  sshift就是ssize的循环次数,也是ssize减1后,1的个数。ssize=16 则sshift=4,ssize=64 则sshift=6。
  全局变量segmentShift就是32-sshift,就是ssize-1的高位0的个数。
  如ssize=16 减一则0000 0000 0000 0000 0000 0000 0000 1111,sshift=4 segmentShift=28
  全局变量segmentMask就是ssize-1,看过了hashmap就知道这个肯定是用来和hash相互&,计算数组下标的
  
1.4 接下来有个判断很有意思
  int c = initialCapacity / ssize;
  if (c  ssize < initialCapacity)
          ++c;
  把c套到下面,就成了((initialCapacity / ssize)  ssize < initialCapacity) = initialCapacity < initialCapacity?
  实在看不懂这么写的意义。
  
1.5 计算segment内的hashentry数组长度,cap
  cap默认为MIN_SEGMENT_TABLE_CAPACITY=2,也就是最少hashentry数组也有2个。
  循环判断(cap < c) cap <<= 1,如initialCapacity=16 / ssize=4,c就是4。hashentry数组长度计算出也就为4
  
1.6 创建segments数组,已经初始化segment[0]。至于为何初始化segment[0],实际就是为了给后面的segment初始化时不用再重复计算。
  默认情况下,s0 loadFactor=0.75,caploadFactor=1 阈值为1,new HashEntry[cap] cap=2
  ss,segments数组 默认大小为ssize=16
  
1.7 使用unsafe包。将s0,赋值到ss[0]
1.8 综上可知,一共有两个数组,一个segments,还有一个segment对象中包含的hashEntry数组。
  两个数组长度都是计算出来的,不是直接指定的。其中segments的长度是ssize,根据concurrencyLevel并发级别计算
  hashEntry长度cap默认为2,如果(cap < c),c是initialCapacity除以segments数组长度。也就是initialCapacity越大,cap也就越大。
  如果initialCapacity = 50。ssize算出得16。c=3,计算得出cap=4,hashEntry数组大小也就为4

2、 put(实际是put了两次)

2.0 unsafe

    在说put之前,有必要说一下unsafe的作用。这是jdk提供的一个安全包,对于了解JMM内存模型的小伙伴都知道,因为我们真实的数据都在内存里,也就是内存条上。CPU读取数据到自己的缓存中。

因为多核cpu,所以每个核心的缓存都是自己单独的一份,每个线程其实都有自己的一个工作缓存,是总线上的一个副本,在并发读写操作的时候,就会出现,A线程对某个数据修改了,回写到总线。

但是B线程在A回写总线之前就取到了这个旧数据,那么B线程就得不到这个数据的最新值。ConcurrentHashMap大量使用了unsafe包进行操作,就是能及时获取总线上的最新值。

主要操作:

UNSAFE.arrayBaseOffset(sc) 获取sc数据的初始偏移量

UNSAFE.arrayIndexScale(CLASS) 获取一个CLASS类的数组一个元素引用占用的大小,用于计算偏移量

UNSAFE.getObject(s, base) 获取s数组中,base偏移量的值

通过这个三个,我们能先获取到一个数组初始偏移量,还有引用偏移量大小,就能用UNSAFE.getObject来获取各个下标的元素值。通过反射获取unsafe包,写个demo

    private static sun.misc.Unsafe UNSAFE;
	private static String[] s = new String[] {"a","b","c","d"};
	static {
		try {
			Field field = Unsafe.class.getDeclaredField("theUnsafe");
			field.setAccessible(true);
			UNSAFE = (Unsafe) field.get(null);
		} catch (Exception e) {
			e.printStackTrace();
		} 
	}

	public static void main(String[] args) {
        // 计算初始偏移量
		int base = UNSAFE.arrayBaseOffset(String[].class);
        // 计算每个元素引用的偏移量,也就是大小
		int index = UNSAFE.arrayIndexScale(String[].class);
		for(int i = 0; i < s.length; i++)
            // 初始偏移量就是0的下标,i*index就是每个元素的偏移量
			System.out.println(UNSAFE.getObject(s, i*index + base));
	}

通过Unsafe就能直接获取到总线内存中的数组的值,防止发生并发,导致缓存不一致。


2.1 定义的一些常量以及两个初始化的局部变量
  int ss:UNSAFE.arrayIndexScale(Segment[].class)  获取一个Segment数组一个元素引用占用的大小,用于计算偏移量
  int ts:UNSAFE.arrayIndexScale(HashEntry[].class)  获取一个HashEntry数组一个元素引用占用的大小,用于计算偏移量


  补充一点,数组中存的是引用。Java对象引用大小是一个非常不确定的值,可能是4个字节或者是8个字节,这个取决于你的JVM设置以及给了多少内存给JVM,
  针对32G以上的堆,它就总是8个字节,但是针对小一点的堆就是4个字节除非你在JVM设置里关掉设置-XX:-UseCompressedOops
  结果就是,安全的方式获取对像引用的大小就是找到Object[]数组中一个元素的大小

  
  long SBASE:UNSAFE.arrayBaseOffset(sc);  获取Segment数组中第一个元素的偏移量
  long TBASE:UNSAFE.arrayBaseOffset(tc);  获取HashEntry数组中第一个元素的偏移量
  
  int SSHIFT:31 - Integer.numberOfLeadingZeros(ss);
  int TSHIFT:31 - Integer.numberOfLeadingZeros(ts);
  这里再说明一下Integer.numberOfLeadingZeros,原理没有研究,就看下用法
  该方法的作用是返回无符号整数i的最高非0位前面的0的个数,包括符号位在内;如果i为负数,这个方法将会返回0,符号位为1。
  比如说,10的二进制表示为 0000 0000 0000 0000 0000 0000 0000 1010 java的整型长度为32位。那么这个方法返回的就是28
  所以,SSHIFT和TSHIFT就是31减去二进制高位0的个数,也就是二进制低位的个数减1,如ss=4,则SSHIFT=2,ss=8,则SSHIFT=3
  
  全局变量segmentMask:1.3 中计算出来的,segment数组的大小-1
  全局变量segmentShift:1.3 中计算出来的,segment数组的大小-1的二进制高位中0的个数

    public V put(K key, V value) {
        Segment<K,V> s;
        // 2.2 value值判断,不能为null
        if (value == null)
            throw new NullPointerException();
        // 2.3 对key进行hash运算
        int hash = hash(key);
        // 2.4 计算key在segment中的数组下标
        int j = (hash >>> segmentShift) & segmentMask;
        // 2.5 判断segment的数组下标中是不是null
        if ((s = (Segment<K,V>)UNSAFE.getObject          
             (segments, (j << SSHIFT) + SBASE)) == null) 
            // 2.6 返回j下标的Segment对象,涉及到2次检查,然后使用CAS更改
            s = ensureSegment(j);
        // 2.7 对HashEntry进行put,涉及到tryLOCK,边try边创建HashEntry,以及HashEntry扩容
        return s.put(key, hash, value, false);
    }

2.2 value值判断,不能为null
2.3 对key进行hash运算,没有判断null。如果key为null会抛出异常
  hash运算时,hashSeed成为一个初始的随机常量,整个ConcurrentHashMap的生命周期中,是唯一常量值。其实估计也是和Segment数组不会扩容有关系
  
  2.4 计算key在segment中的数组下标
  (hash >>> segmentShift) & segmentMask;
  看过hashMap后这个 & segmentMask相与就很容量理解。
  (hash >>> segmentShift) 就是右移 segment数组的大小-1的二进制高位中0的个数,
  比较绕,换言之就是留下hash值的高位放到低位,然后和segmentMask计算数组下标。这个和在计算HashEntry下标不同,HashEntry与HashMap是相同的
  为什么这么做,因为,如果对hash的低位和segment数组大小减1计算。hash的低位和HashEntry数组大小减1计算。
  如果出现,segment数组大小同HashEntry数组大小一致时,两个数组下标就一致了。
  一旦一致就出现,先放入segment[5],再放入HashEntry[5]或者先放入segment[6],再放入HashEntry[6]这种情况。浪费了HashEntry数组的空间
  
  2.5 判断segment的数组下标中是不是null
  这个判断也很绕。UNSAFE.getObject(segments, (j << SSHIFT) + SBASE))
  正常使用UNSAFE是这样写,UNSAFE.getObject(s, base + j*ns),s是数组,base是初始位置,ns是单个引用大小,j表示取第j个元素。
  实际这两个是同一个意思jns = j<<SSHIFT。引用其实要么4,要么8.如果引用是4,SSHIFT=2。左移两位就是乘4。是一个原理不同写法
  
  2.6 ensureSegment方法,返回j下标的Segment对象

 创建并初始化segment对象,创建其中的hashEntry数组,int k为在segment数组中的下标


  2.6.1 long u:根据segment数组下标计算偏移量


  2.6.2 用UNSAFE的getObjectVolatile方法获取总线中的第k个数组下标。

  防止工作缓存,
  如果为空,没有被并发创建,获取之前初始化的segment[0]上的参数
  cap:0上的hashEntry长度  lf:0上的加载因子  threshold:根据长度加载因子算的阈值
  根据参数创建出新的hashEntry数组,再次用getObjectVolatile判断出是否在下标上已有数组,重新检查
  如果还是为空,就使用while+cas进行把新创建的hashEntry在segment[k]位置上赋值并返回
  如果在两次判断中有一次不为空,就进行返回其他线程创建的segment对象
  
  2.7 对HashEntry进行put
  每一个segment都有一个HashEntry数组,Segment继承了ReentrantLock可重入锁,所以可以进行lock操作
  2.7.1 进行tryLock操作,先看一次就成功的情况。
  加锁成功,根据hashEntry的长度&key的hash,计算出在hashEntry中的数组下标。
  拿到当前下标中的对象,也是链表中的头结点。对链表遍历,发现了之前的key,就进行替换value操作,跳出遍历
  遍历完没有发现key,在判断note在之前tryLock等待的时候有无被创建,被创建了,就将next指向链表的头结点。
  数组已存元素长度+1,如果大于了阈值并且没有超过最大限制,判断是否扩容。否则就直接头插法插入。跳出遍历
  
  2.7.2 tryLock失败,循环尝试加锁,并预先创建note。scanAndLockForPut(意义并不大)
  就是在加锁失败的时候,看note还是null,如果发现了链表中存在了相同的key,就不做操作,否则就去预创建note
  循环加锁次数看cpu核心数,大于1就是64次,否则1次,然后进入内核态
  也有判断头结点是否有变化,有变化就重新从头结点再遍历一次,看链表有没有相同的key。
  如果获取到锁,就结束方法。(存在一个问题,比如预创建了一个note,然后和其他并发插入的线程是一个key,这个note也没有后续操作,多了个没用的元素)
  
  2.7.3 HashEntry扩容
  扩容如果看了HashMap的扩容,那么这个就非常简单。
  先对数组翻倍创建一个新的数组,计算新的阈值,计算hash也不会再有重新打乱hash,而是和之前的算法保持一致
  这样就能知道,每个链表的元素。只有两个下标的可能,要么是当前下标,要么是当前下标加扩容的大小的值。
  唯一有点不同就是,扩容的时候在老数组上会循环遍历各个下标的链表
  刚进入每个链表的时候,就出现了一段找相同下标的代码。这段代码意思我理解就是,尽可能第一次移走多个连续的相同下标的note
  然后再挨个遍历链表,计算下标,头插法移到新数组。

三、JDK1.8 HashMap

终于将到jdk1.8的hashMap了,以前只是知道红黑树是个啥,里面细节从来没看过,这次专门沉下心看了看,读着代码按规则走勉强能看懂吧,配合这个网址,多插入试验几次。

勉强把插入和左旋右旋读了下,删除真的没有读。有兴趣的小伙伴可以专门找一篇数据结构方面的,我这里就只是把大概红黑树的样子描述下,做个抛砖引玉吧

一、引入红黑树概念:(实现挺繁琐的,就只先看规则)

规则:
1、每个节点要么黑,要么红
2、根节点是黑的
3、叶子节点都是黑的(实际叶子节点都是null的黑节点)
4、如果一个节点是红的,那么自己的子节点都是黑的
5、对每个结点,从自己到其他任何叶子节点的路径上都有相同数量的黑节点
特性(3)中的叶子节点,是只为空(NIL或null)的节点。
特性(5),确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树。时间复杂度是O(lgn)

调整规则:
旋转:

x的右节点是y,进行左旋,意味着"将x变成y的左节点,y的左节点变成x的右节点,x之前的父节点变成y的父节点"。
x的左节点是y,进行右旋,意味着"将x变成y的右节点,y的右节点变成x的左节点,x之前的父节点变成y的父节点"。

左旋示意图:               z        右旋示意图:           y
   x                             /                      x                       \                 
  / \    --(左旋)-->      x                      / \    --(右旋)-->     x
y   z                      /                        y   z                          \
                          y                                                             z
                       
左旋:伪代码LEFT-ROTATE(T, x)  

01  y ← right[x]            // 前提:这里假设x的右孩子为y。下面开始正式操作
02  right[x] ← left[y]      // 将 “y的左孩子” 设为 “x的右孩子”,即 将β设为x的右孩子
03  p[left[y]] ← x          // 将 “x” 设为 “y的左孩子的父亲”,即 将β的父亲设为x
04  p[y] ← p[x]             // 将 “x的父亲” 设为 “y的父亲”
05  if p[x] = nil[T]       
06  then root[T] ← y                 // 情况1:如果 “x的父亲” 是空节点,则将y设为根节点
07  else if x = left[p[x]]  
08            then left[p[x]] ← y    // 情况2:如果 x是它父节点的左孩子,则将y设为“x的父节点的左孩子”
09            else right[p[x]] ← y   // 情况3:(x是它父节点的右孩子) 将y设为“x的父节点的右孩子”
10  left[y] ← x             // 将 “x” 设为 “y的左孩子”
11  p[x] ← y                // 将 “x的父节点” 设为 “y”

右旋:伪代码RIGHT-ROTATE(T, x)  

01  x ← left[y]             // 前提:这里假设y的左孩子为x。下面开始正式操作
02  left[y] ← right[x]      // 将 “x的右孩子” 设为 “y的左孩子”,即 将β设为y的左孩子
03  p[right[x]] ← y         // 将 “y” 设为 “x的右孩子的父亲”,即 将β的父亲设为y
04  p[x] ← p[y]             // 将 “y的父亲” 设为 “x的父亲”
05  if p[y] = nil[T]       
06  then root[T] ← x                 // 情况1:如果 “y的父亲” 是空节点,则将x设为根节点
07  else if y = right[p[y]]  
08            then right[p[y]] ← x   // 情况2:如果 y是它父节点的右孩子,则将x设为“y的父节点的左孩子”
09            else left[p[y]] ← x    // 情况3:(y是它父节点的左孩子) 将x设为“y的父节点的左孩子”
10  right[x] ← y            // 将 “y” 设为 “x的右孩子”
11  p[y] ← x                // 将 “y的父节点” 设为 “x”

插入:
将一个节点插入到红黑树中,首先,将红黑树当作一颗二叉查找树,将节点插入;然后,将节点着色为红色;最后,通过旋转和重新着色等方法来修正该树,使之重新成为一颗红黑树。
1、将红黑树当作一颗二叉查找树,将节点插入。
红黑树本身就是一颗二叉查找树,将节点插入后,该树仍然是一颗二叉查找树。也就意味着,树的键值仍然是有序的。
此外,无论是左旋还是右旋,若旋转之前这棵树是二叉查找树,旋转之后它一定还是二叉查找树。
这也就意味着,任何的旋转和重新着色操作,都不会改变它仍然是一颗二叉查找树的事实。
2、将插入的节点着色为"红色"。到了这一步,其实之前的规则中,只有4(如果一个节点是红的,那么自己的子节点都是黑的)不满足了,接下来的操作就是为了使4满足
3、通过一系列的旋转或着色等操作,使之重新成为一颗红黑树。

添加操作:伪代码
RB-INSERT(T, z)  

01  y ← nil[T]                        // 新建节点“y”,将y设为空节点。
02  x ← root[T]                       // 设“红黑树T”的根节点为“x”
03  while x ≠ nil[T]                  // 找出要插入的节点“z”在二叉树T中的位置“y”
04      do y ← x                      
05         if key[z] < key[x]  
06            then x ← left[x]  
07            else x ← right[x]  
08  p[z] ← y                          // 设置 “z的父亲” 为 “y”
09  if y = nil[T]                     
10     then root[T] ← z               // 情况1:若y是空节点,则将z设为根
11     else if key[z] < key[y]        
12             then left[y] ← z       // 情况2:若“z所包含的值” < “y所包含的值”,则将z设为“y的左孩子”
13             else right[y] ← z      // 情况3:(“z所包含的值” >= “y所包含的值”)将z设为“y的右孩子” 
14  left[z] ← nil[T]                  // z的左孩子设为空
15  right[z] ← nil[T]                 // z的右孩子设为空。至此,已经完成将“节点z插入到二叉树”中了。
16  color[z] ← RED                    // 将z着色为“红色”
17  RB-INSERT-FIXUP(T, z)             // 通过RB-INSERT-FIXUP对红黑树的节点进行颜色修改以及旋转,让树T仍然是一颗红黑树

主要操作就是去查找自己插入时的父节点,如果大于父节点,就是左节点,否则是右节点。然后将自己设为红色,另外插入肯定是直接到根节点上,和链表会有中间的插入不同

调整操作:伪代码
RB-INSERT-FIXUP(T, z)

01 while color[p[z]] = RED                                                  // 若“当前节点(z)的父节点是红色”,则进行以下处理。
02     do if p[z] = left[p[p[z]]]                                           // 若“z的父节点”是“z的祖父节点的左孩子”,则进行以下处理。
03           then y ← right[p[p[z]]]                                        // 将y设置为“z的叔叔节点(z的祖父节点的右孩子)”
04                if color[y] = RED                                         // Case 1条件:叔叔是红色
05                   then color[p[z]] ← BLACK                    ▹ Case 1   //  (01) 将“父节点”设为黑色。
06                        color[y] ← BLACK                       ▹ Case 1   //  (02) 将“叔叔节点”设为黑色。
07                        color[p[p[z]]] ← RED                   ▹ Case 1   //  (03) 将“祖父节点”设为“红色”。
08                        z ← p[p[z]]                            ▹ Case 1   //  (04) 将“祖父节点”设为“当前节点”(红色节点)
09                   else if z = right[p[z]]                                // Case 2条件:叔叔是黑色,且当前节点是右孩子
10                           then z ← p[z]                       ▹ Case 2   //  (01) 将“父节点”作为“新的当前节点”。
11                                LEFT-ROTATE(T, z)              ▹ Case 2   //  (02) 以“新的当前节点”为支点进行左旋。
12                           color[p[z]] ← BLACK                 ▹ Case 3   // Case 3条件:叔叔是黑色,且当前节点是左孩子。(01) 将“父节点”设为“黑色”。
13                           color[p[p[z]]] ← RED                ▹ Case 3   //  (02) 将“祖父节点”设为“红色”。
14                           RIGHT-ROTATE(T, p[p[z]])            ▹ Case 3   //  (03) 以“祖父节点”为支点进行右旋。
15        else (same as then clause with "right" and "left" exchanged)      // 若“z的父节点”是“z的祖父节点的右孩子”,将上面的操作中“right”和“left”交换位置,然后依次执行。
16 color[root[T]] ← BLACK

如果父节点是红的处理方式,父节点如果是黑的,就直接插入,不用调整操作
如果父节点是左红节点:
1、叔叔不为空是红色->设置,父节点为黑色,叔叔为黑色,爷爷为红色,并且设置自己为爷爷节点重新循环
2、叔叔是黑色右节点,设置父节点为当前节点,以当前节点左旋
3、叔叔是黑色左节点,设置父节点为黑色,爷爷节点设置为红色,以爷爷节点为支点右旋。
如果父节点是右红节点:
1、叔叔不为空是红色->设置,父节点为黑色,叔叔为黑色,爷爷为红色,并且设置自己为爷爷节点重新循环
2、叔叔是黑色左节点,设置父节点为当前节点,以当前节点右旋
3、叔叔是黑色右节点,设置父节点为黑色,爷爷节点设置为红色,以爷爷节点为支点左旋。

 

四、JDK1.8 ConcurrentHashMap

1.7ConcurrentHashMap先把大致流程写了下,对照代码注解再等。剩下1.8的还没更新,抽时间更新,原创不易,注解都是自己码的,给个点赞关注谢谢~

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值