Java7与Java8中的HashMap和ConcurrentHashMap知识点总结

JAVA7中的ConcurrentHashMap简介

Java7的ConcurrentHashMap里有多把锁,每一把锁用于其中一部分数据,那么当多线程访问容器里不一样数据段的数据时,线程间就不会存在锁竞争,从而能够有效的提升并发访问效率呢。这就是“锁分离”技术。html

ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁(继承了ReentrantLock),在ConcurrentHashMap里扮演的角色,HashEntry则用于存储键值对数据。java

 

Java7中的ConcurrentHashMap底层逻辑结构

一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap相似,是一种数组和链表结构,Segment数组每一个Segment里包含一个HashEntry数组,一个HashEntry数组中的每一个hashEntry对象是一个链表的头结点每一个链表结构中包含的元素才是Map集合中的key-value键值对。以下图:node

 

 

由上图可见,ConcurrentHashMap的数据结构;一个Segment对应(锁住)一个HashEntry数组。算法

每一个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先得到它对应的Segment锁数据库

肯定插入的元素在哪个Segment的位置,固然也是Hash,这个算法也是左移、右移等,而后再插入到具体的HashEntry数组中。数组

Java7中ConcurrentHashMap使用的就是锁分段技术,ConcurrentHashMap由多个Segment组成(Segment下包含不少Node,也就是咱们的键值对了),每一个Segment都有把锁来实现线程安全,当一个线程占用锁访问其中一个段数据的时候,其余段的数据也能被其余线程访问。缓存

所以,关于ConcurrentHashMap就转化为了对Segment的研究。这是由于,ConcurrentHashMap的get、put操做是直接委托Segmentget、put方法。安全

 

JAVA8中的ConcurrentHashMap

我的的理解

Java8中的锁的粒度比Java7中更细了,Java8锁住的一个某一个数组元素table[i](头节点,该头结点类型是链表头结点或红黑树的头结点),而Java7中segment锁住的是一个HashEntry数组,至关于锁住了多个数组元素;因此我感受Java8中ConcurrentHashMap多线程环境下 put效率更高。数据结构

 

HashMap核心数据结构之一Node

先来看一下HashMap(先理解HashMap再看ConcurrentHashMap更容易)集合底层的数组Node[] table的某一个元素table[i](即某一个链表的头结点),表示Map集合一些泛型对象构成的链表或者红黑树多线程

 

static class Node<K,V> implements Map.Entry<K,V> { final int hash; //用来定位数组索引位置 final K key; V value; Node<K,V> next; //链表的下一个node Node(int hash, K key, V value, Node<K,V> next) { ... } public final K getKey(){ ... } public final V getValue() { ... } public final String toString() { ... } public final int hashCode() { ... } public final V setValue(V newValue) { ... } public final boolean equals(Object o) { ... } } 

 

  

(重点)HashMap的基本方法原理

HashMap为何链表长度大于8就要转红黑树?

红黑树的插入、删除和遍历的最坏时间复杂度都是log(n),所以,意外的状况或者恶意使用下致使hashcode()方法的返回值不好时,只要Key具备可比性,性能的降低将会是"优雅"的。
但因为TreeNodes的空间占用是常规Nodes两倍,因此只有桶中包含足够多的元素以供使用时,咱们才应该使用树。那么为何这个数字会是8呢?

官方文档的一段描述:

Because TreeNodes are about twice the size of regular nodes, we use them only when bins contain enough nodes to warrant use (see TREEIFY_THRESHOLD). And when they become too small (due to removal or resizing) they are converted back to plain bins. In usages with well-distributed user hashCodes, tree bins are rarely used. Ideally, under random hashCodes, the frequency of nodes in bins follows a Poisson distribution (http://en.wikipedia.org/wiki/Poisson_distribution) with a parameter of about 0.5 on average for the default resizing threshold of 0.75, although with a large variance because of resizing granularity. Ignoring variance, the expected occurrences of list size k are (exp(-0.5) * pow(0.5, k) / factorial(k)). The first values are:

0: 0.60653066
1: 0.30326533
2: 0.07581633
3: 0.01263606
4: 0.00157952
5: 0.00015795
6: 0.00001316
7: 0.00000094
8: 0.00000006 more: less than 1 in ten million

理想状况下,在随机哈希代码下,桶中的节点频率遵循泊松分布,文中给出了桶长度k的频率表。
由频率表能够看出,同一个Hash数组位置冲突达到8几率很是很是小(一亿分之六)。因此做者应该是根据几率统计而选择了8做为阀值,因而可知,这个选择是很是严谨和科学的。

因此链表转红黑树几率很低;可是一旦元素个数大于8链表的查询删除(删除要先查询到才能删)效率综合来看比不上红黑树的效率(本文末有对红黑树简单介绍)。

 

Java8 HashMap的hash算法

若是key为null,hash值为0;若是不为null,直接将key的hashcode值hh无符号右移16位后的数进行按位异或^位运算 

 

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

 

ConcurrentHashMap的计算Hash值函数和HashMap不太同样

 

static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }
......
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash

  

 

(重点)Java8解决Java7中HashMap多线程状况下形成的遍历元素死循环

Java7 HashMap put元素后超过了阈值,就须要扩容,此时若是有多线程同时扩容(resize调用transfer,会倒排原有的一个链表中元素的顺序,由于是头插法,好比原来的顺序是A->B->C,扩容后就多是A,B->A,C->B->A,而另外一个线程来扩容也是头插法为C,B->C,A->B->C,第一个线程有C->B第二个线程B->C,而这里造成了环形链表)状况下,其中一个A线程挂起,而另外的线程完成扩容后,而后A线程再也不挂起,继续扩容,会造成一个环形链表,下一次调用get方法或put方法遍历这个Hash数组的位置的链表,若是元素key不存在,就在这个循环链表中无限循环遍历了,由于不存在尾结点的指针域为null中止循环遍历了。

Java8中的HashMap put时若是有冲突须要插入到链表尾部,Java7是插入到头部;

无论是头插入仍是尾部插入,都是须要遍历对应数组位置的全部链表或红黑树节点,由于若是key存在,是更新操做,返回oldValue;

Java8,除了对hashmap增长红黑树结果外,对Java7原有形成死锁的关键缘由点(扩容时新table复制在头端添加元素)改进为依次在末端添加新的元素;

Java8利用红黑树改进了链表过长查询遍历慢问题,多线程resize时保持原来链表的元素顺序(  loTail、hiTail尾部插入再也不根据key的hash值从新散列到新数组,而是放到原数组下标或下标为(原数组下标+oldCapacity老数组容量)的位置,见Java8 HashMap resize源码),避免出现致使put产生循环链表的bug。

图解:https://blog.csdn.net/linsongbin1/article/details/54708694

 

Java8HashMap resize部分源码

(重点)下面代码中的"if ((e.hash & oldCap) == 0) "这里按位于位运算,其实是判断元素的key的hash值与原来老数组的容量的关系来决定扩容后元素放在新数组的下标;

好比老容量为16,二进制表示10000,若是hash值表示的二进制数从右往左数在第5位为1,则e.hash & oldCap 一定为10000不为0,而若是hash值表示的二进制数从右往左数在第5位为0,则则e.hash & oldCap 一定为0

好比老数组容量为16,A元素的hash值是17,那在原数组中A的下标为j = 17 & (16 - 1) = 1, 扩容后若是是正常散列17应该放在 17 & (32 -1)= 17的位置,而e.hash & oldCap 即17 & 16等于16不等于0,因此扩容后的位置为j+oldCap=17(j =1, oldCap = 16);

好比老数据容量为16,A元素的hash值是33,那在原数组中A的下标为j = 33 & (16 - 1) = 1, 扩容后若是是正常散列33应该放在 33 & (32 -1)= 1的位置,而e.hash & oldCap 即33 & 16等于0,因此扩容后的位置仍然是为j(j =1);

能够简单体会下扩容位运算判断元素散列到原来老数组这一半的下标,仍是新数组这一半的下标,与元素正常散列到hash新数组的关系。

final Node<K,V>[] resize() { 省略部分代码... 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; // (重点)能够简单体会下扩容位运算判断元素散列到原来老数组这一半的下标,仍是新数组这一半的下标,与正常散列到hash新数组的关系。
                            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; } 

 

JDK1.8的ConcurrentHashMap中Segment

JDK1.8的ConcurrentHashMap中Segment虽保留,但已经简化属性,仅仅是为了兼容旧版本

/** * Stripped-down version of helper class used in previous version, * declared for the sake of serialization compatibility */
    static class Segment<K,V> extends ReentrantLock implements Serializable { private static final long serialVersionUID = 2249069246763182397L; final float loadFactor; Segment(float lf) { this.loadFactor = lf; } }

 

JAVA8 ConcurrentHashMap与HashMap的一些相通之处

JAVA8中ConcurrentHashMap的底层与Java8的HashMap有相通之处,底层依然由“数组”+链表+红黑树来实现的,底层结构存放的是TreeBin对象,而不是TreeNode对象;TreeBin包装的不少TreeNode节点,它代替了TreeNode的根节点,也就是说在实际的ConcurrentHashMap“数组”中,存放的是TreeBin对象,而不是TreeNode对象,这是与HashMap的区别之一。

 

ConcurrentHashMap使用CAS更新value

ConcurrentHashMap实现中借用了较多的CAS(Compare And Swap)算法(sun.misc.Unsafe对象中有不少native本地方法,如unsafe.compareAndSwapInt(this, valueOffset, expect, update))。

意思是若是valueOffset位置包含的值与expect相同,则更新valueOffset位置的值为update,并返回true,不然不更新,返回false。

我理解为若是将要更新的变量的值若是和这个线程从Map中取出值相同,那么就更新,不然就不更新(和CAS的思想同样)。

 

ConcurrentHashMap使用了synchronized关键字进行同步

ConcurrentHashMap既然借助了CAS来实现非阻塞的无锁实现更改value线程安全,那么是否是就没有用锁了呢??答案:仍是使用了synchronized关键字进行同步了的,在哪里使用了呢?在操做同一Node数组下标的链表或红黑树头结点仍是会synchronized上锁,这样才能保证线程安全。

 

(重点)为何须要synchronized关键字进行同步?

 

由于若是不锁住该位置的头结点,当一个线程在对该Hash数组该位置的链表或者红黑树进行操做时,若是其余线程操做(修改,添加元素,删除等)引发了Map的resize(扩容或缩减),该链表或红黑树hash值可能会发生变化,而正在进行写操做(如put)的线程会由于hash值改变而找不到该位置对于的元素,还有例如插入到当前尾结点后面,若是当前尾结点正好被删除了就会有问题

锁住了头结点,其余线程就没法操做(修改,添加元素,删除等)该链表或者红黑树,当持有锁的线程操做完毕,释放头结点锁,其余线程才有机会得到该位置的锁,而后进行操做。

 

ConcurrentHashMap整个类的源码,和HashMap的实现基本如出一辙,当有修改操做时借助了synchronized来对table[i](头结点)进行锁定保证了线程安全以及使用了CAS来保证原子性操做,其它的基本一致,例如:ConcurrentHashMap的get(int key)方法的实现思路为:根据key的hash值找到其在table所对应的位置i,而后在table[i]位置所存储的链表(或者是树)进行查找是否有键为key的节点,若是有,则返回节点对应的value,不然返回null。

 

(重点)为何要指定HashMap的容量?

首先建立HashMap指定容量好比1024后,并非HashMap的size不是1024,而是0,插入多少元素,size就是多少;

而后若是不指定HashMap的容量,要插入768个元素,第一次容量为16,须要持续扩容屡次到1024,才能保存1024*0.75=768个元素;

HashMap扩容是很是很是消耗性能的,Java7中每次先建立2倍当前容量的新数组,而后再将老数组中的全部元素次经过hash&(length-1)的方式散列到HashMap中;

Java8中HashMap虽然不须要从新根据hash值散列后的新数组下标,可是因为须要遍历每一个Node数组的链表或红黑树中的每一个元素,根据元素key的hash值原来老数组的容量的关系来决定放到新Node数组哪一半(2倍扩容),仍是须要时间的。

 

(重点)HashMap指定容量初始化后,底层Hash数组已经被分配内存了吗?

Java8 HashMap指定容量构造方法源码,会调整threshold2的整数次幂,好比指定容量为1000,则threshold为1024

public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); } /** * Returns a power of two size for the given target capacity. */
 static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }

能够根据源码看到,在指定的HashMap初始容量后,底层的Hash数组Node<K,V>[] table此时并无被初始化,依旧为null;

那么是何时被初始化的呢?见:https://www.cnblogs.com/theRhyme/p/11763685.html

 

 

ConcurrentHashMap类中核心属性的介绍

 

sizeCtl最重要的属性之一,看源码以前,这个属性表示什么意思,必定要记住。

private transient volatile int sizeCtl;//控制标识符

 

sizeCtl是控制标识符,不一样的值表示不一样的意义。

 

  • 负数表明正在进行初始化扩容操做 ,其中-1表明正在初始化 ;-N(N>1) 表示有N-1个线程正在进行扩容操做
  • 0表示未被初始化
  • 正数表示下一次进行扩容的大小,sizeCtl的值 = 当前ConcurrentHashMap容量*0.75,这与loadfactor是对应的。实际容量>=sizeCtl,则扩容。

 

 

一、 transient volatile Node<K,V>[] table;

 

是一个Node数组,第一次插入数据的时候初始化,大小是2的幂次方。这就是咱们所说的底层结构:”数组+链表(或树)”。

注意这里的Node数组加了volatile(volatile关键字的做用进行修饰,table数组在内存中对全部线程都及时可见,若是一个线程修改了table数组的值,其余线程中若是本身的线程栈中有table的副本,就会把table缓存行设置为失效,强制从内存中读取table数组的值。

因此一个线程调用ConcurrentHashMap的get方法也能得到最新的Map集合元素的值(迭代器多是旧值)。

 

 

二、private static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量

三、private static final intDEFAULT_CAPACITY = 16;

四、static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; // MAX_VALUE=2^31-1=2147483647

五、private static finalint DEFAULT_CONCURRENCY_LEVEL = 16;

六、private static final float LOAD_FACTOR = 0.75f;

七、static final int TREEIFY_THRESHOLD = 8; // 链表转树的阈值,若是table[i]下面的链表长度大于8时就转化为数

八、static final int UNTREEIFY_THRESHOLD = 6; //树转链表的阈值,小于等于6是转为链表,仅在扩容tranfer时才可能树转链表

九、static final int MIN_TREEIFY_CAPACITY = 64;

十、private static final int MIN_TRANSFER_STRIDE = 16;

十一、private static int RESIZE_STAMP_BITS = 16;

十二、private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1; // help resize的最大线程数

1三、private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

1四、static final int MOVED = -1; // hash for forwarding nodes(节点在Map中的hash值)、标示位

1五、static final int TREEBIN = -2; // hash for roots of trees(树根节点的hash值

1六、static final int RESERVED = -3; // hash for transient reservations(保留节点的hash值,为了扩容的时候吧)

 

在ConcurrentHashMap中有一个构造方法中有一个参数:concurrencyLevel,表示可以同时更新ConccurentHashMap且不产生锁竞争的最大线程数默认值为16,(即容许16个线程并发可能不会产生竞争)。为了保证并发的性能,咱们要很好的估计出concurrencyLevel值,否则要么竞争至关厉害,从而致使线程试图写入当前锁定的段时阻塞。

 

ConcurrentHashMap的get()方法没有对任何对象加锁,因此可能会读到的数据(好比经过迭代器遍历的时候,其余线程修改了数组元素),和HashMap的get方法的原理是如出一辙的。 

 

ConcurrentHashMap链表转树时,并不会直接转,正如注释(Nodes for use in TreeBins)所说,只是把这些节点包装成TreeNode放到TreeBin中,再由TreeBin来转化红黑树

 

ConcurrentHashMap与HashMap的一些区别

ConcurrentHashMap不容许null key或value,HashMap能够;

ConcurrentHashMap的计算Hash值函数也和HashMap不同:

还有一些如前所说的一些和其余区别,就不打算详细说明了。

 

(重点)ConcurrentHashMap的putVal方法

putVal(K key, V value, boolean onlyIfAbsent, boolean evict)大致流程以下:
一、检查key/value是否为null,若是为null,则抛异常,不然进行2
二、进入for死循环,进行3
三、检查table是否初始化了,若是没有,则调用initTable()进行初始化而后进行 2,不然进行4
四、根据key的hash值计算出其应该在table中储存的位置i(根据key的hashcode计算出Hash值,在将Hash值与length-1进行按位与,length是2的整数次幂,减1后的二进制与Hash值进行按位与至关于取余运算,但取余的位运算次数确定不止1次,而这里一次位运算就得出结果效率更高),取出table[i]的节点用f表示。
根据f的不一样有以下三种状况:

  1)若是table[i]==null(即该位置的节点为空,没有发生碰撞),则利用CAS操做直接存储在该位置,若是CAS操做成功则退出死循环
  2)若是table[i]!=null(即该位置已经有其它节点,发生碰撞),碰撞处理也有两种状况
    2.1)检查table[i]的节点的hash是否等于MOVED(-1),若是等于,则检测到正在扩容,则帮助其扩容
    2.2)说明table[i]的节点的hash值不等于MOVED,synchronized锁住头结点table[i],进行插入操做;

        若是table[i]为链表节点,则将此节点插入链表末尾中便可;

        若是table[i]为树节点,则将此节点插入树中便可;

        插入成功后,进行 5

五、若是table[i]的节点是链表节点,则检查table的第i个位置的链表的元素个数是否大于了8,大于8就须要转化为,若是须要则调用treeifyBin函数进行转化。
  链表转树:将数组tab的第index位置的链表转化为红黑树。
六、插入成功后,若是key已经存在,返回oldValue;key开始不存在,返回null

 

上面第4点中的2)中的2.1)帮助扩容:若是当前正在扩容,则尝试协助其扩容死循环再次发挥了重试的做用,有趣的是ConcurrentHashMap是能够多线程同时扩容的。

这里说协助的缘由在于,对于数组扩容,通常分为两步:1.新建一个更大的数组;2.将原数组数据(从新散列Hash值)copy到新数组中

对于第一步,ConcurrentHashMap经过CAS来控制一个int变量保证新建数组这一步建一个更大的数组只会执行一次;

对于第二步,ConcurrentHashMap采用CAS + synchronized + 移动后标记 的方式来达到多线程扩容的目的。

感兴趣能够查看transfer函数。 

目前的猜测多线程扩容多是多线程操做不一样的table位置的链表或红黑树,检查table[i]的节点的hash是否等于MOVED(-1),若是是帮助其扩容,将元素从新散列新的table数组的对应位置中。

 

ConcurrentHashMap实现高效的并发操做的3个函数(与sun.misc.Unsafe U对象有关)

这得益于ConcurrentHashMap中的以下三个函数(sun.misc.Unsafe U)

/* 3个用的比较多的CAS操做 */ @SuppressWarnings("unchecked") // ASHIFT等均为private static final  static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) { // 获取索引i处Node  return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE); } // 利用CAS算法设置i位置上的Node节点(将ctable[i]比较,相同则插入v)。  static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) { return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v); } // 在链表转树时用到了这个方法;设置节点位置的值,仅在上锁区被调用  static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) { U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);  }

 

 

 

private final void treeifyBin(Node<K,V>[] tab, int index) { …… } treeifyBin方法的思想:检查下table的长度Map的容量,不是乘以加载因子的那个,也不是目前集合中元素的个数)是否大于等于MIN_TREEIFY_CAPACITY(64),若是不大于,
则调用tryPresize方法将table两倍扩容就能够了,就不降链表转化为树了;若是大于,则就将table[i]链表转化为树

 

 

public long mappingCount() { long n = sumCount(); return (n < 0L) ? 0L : n; // ignore transient negative values } 这是Java8中查询元素个数的方法mappingCount,返回值是long;这个应该用来代替size()方法被使用。这是由于ConcurrentHashMap可能比包含更多的映射结果,即超过int类型的
最大值(size()方法的返回值是int);这个方法返回值是一个估计值,因为存在并发的插入删除,所以返回值可能与实际值会有出入。

 

 

final long sumCount() { CounterCell[] as = counterCells; CounterCell a; long sum = baseCount; if (as != null) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) sum += a.value; } } return sum; }
元素个数

HashMap中的迭代器为何会抛出 ConcurrentModificationException异常(源码解析)?

HashMap不是线程安全的,其中的缘由之一就是:在多线程环境下,一个线程经过迭代器遍历或删除时,另外一个线程修改了HashMap,则modCount++,形成迭代器中的expectedModCount与HashMap中的modCount不同。

HashMap中迭代器源码以下:

final class EntryIterator extends HashIterator implements Iterator<Map.Entry<K,V>> { public final Map.Entry<K,V> next() { return nextNode(); } }
abstract class HashIterator { Node<K,V> next;        // next entry to return
        Node<K,V> current;     // current entry
        int expectedModCount;  // for fast-fail
        int index;             // current slot
 HashIterator() { expectedModCount = modCount; Node<K,V>[] t = table; current = next = null; index = 0; if (t != null && size > 0) { // advance to first entry
                do {} while (index < t.length && (next = t[index++]) == null); } } public final boolean hasNext() { return next != null; } final Node<K,V> nextNode() { Node<K,V>[] t; Node<K,V> e = next; if (modCount != expectedModCount) throw new ConcurrentModificationException(); if (e == null) throw new NoSuchElementException(); if ((next = (current = e).next) == null && (t = table) != null) { do {} while (index < t.length && (next = t[index++]) == null); } return e; } public final void remove() { Node<K,V> p = current; if (p == null) throw new IllegalStateException(); if (modCount != expectedModCount) throw new ConcurrentModificationException(); current = null; K key = p.key; removeNode(hash(key), key, null, false, false); expectedModCount = modCount; } }

 

ConcurrentHashMap 中的迭代器(弱一致)

遍历过程当中,若是已经遍历的数组上的内容变化了,迭代器不会抛出 ConcurrentModificationException 异常。

若是未遍历的数组上的内容发生了变化,则有可能反映到迭代过程当中。

这就是 ConcurrentHashMap 迭代器弱一致的表现。

在这种迭代方式中,当 iterator 被建立后,集合再发生改变就再也不是抛出 ConcurrentModificationException,取而代之的是在改变时 new 新的数据 从而不影响原有的数据,iterator 完成后再将头指针替换为新的数据,这样 iterator 线程可使用原来老的数据,而写线程也能够并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提高的关键。 总结,ConcurrentHashMap 的弱一致性主要是为了提高效率,是一致性 与效率之间的一种权衡。

 

红黑树简单介绍


算法导论中lgn默认都是以2为底的,即为log2n


红黑树使二叉搜索树更加平衡:红黑树是一种二叉搜索树,但在每一个节点上增长一个存储位表示节点的颜色,但是 red 或 black,红黑树的查找、插入、删除的时间复杂度最坏为 O(lgn)

 

 

(重点)树的高度决定查询性能,让树尽量平衡,就是为了下降树的高度

 

 

由于由 n 个节点随机生成的二叉搜索树高度 lgn,因此二叉搜索树的通常操做的执行时间为 O(lgn)


红黑树是牺牲了严格的高度平衡的优越条件为代价,红黑树可以以O(log2 n)的时间复杂度进行搜索、插入、删除操做。

此外,因为它的设计,任何不平衡都会在三次旋转以内解决。


红黑树的查询性能略微逊色于AVL(平衡二叉查找树)树,由于他比avl树会稍微不平衡最多一层,也就是说红黑树的查询性能只比相同内容的avl树最多多一次比较

可是,红黑树在插入和删除上完胜avl树,avl树每次插入删除会进行大量的平衡度计算,而红黑树为了维持红黑性质所作的红黑变换和旋转的开销,相较于avl树为了维持平衡的开销要小得多


红黑树的特性大体有三个(换句话说,插入、删除节点后整个红黑树也必须知足下面的三个性质,若是不知足则必须进行旋转):
1.根节点叶节点都是黑色节点,其中叶节点为Null节点
2.每一个红色节点的两个子节点都是黑色节点,换句话说就是不能有连续两个红色节点,可是黑色节点能够连续
3.从节点到全部叶子节点上的黑色节点数量是相同

上述的性质约束了红黑树的关键:从根到叶子的最长可能路径很少于最短可能路径的两倍长。

获得这个结论的理由是:
红黑树中最短的可能路径是所有为黑色节点的路径;
红黑树中最长的可能路径是红黑相间的路径。

 

文件系统和数据库的索引为何用的是B+树而不是红黑树或B树

https://www.toutiao.com/a6739784316991046155/?timestamp=1569399352&app=news_article&group_id=6739784316991046155&req_id=2019092516155101002607708223015B85

B树与红黑树比较:

内存中,红黑树效率更高,可是涉及到磁盘操做B树更优;

文件系统和数据库的索引都是存在硬盘上的,而且若是数据量大的话,不必定能一次性加载到内存中,B树能够根据索引每次加载一个节点,再继续往下找;

B+树的多路存储非叶结点保存的索引,而红黑树全部节点保存的是结果值;

红黑树是二叉的,保存大量记录树高度过高了,查询效率不高。

 

B+树与B树比较

B 无论叶子节点仍是非叶子节点,都会保存数据,这样致使在非叶子节点中能保存的指针数量变少

B树指针少的状况下要保存大量数据,只能增长树的高度,致使 IO 操做变多,查询性能变低;

树的高度决定查询性能,就是为了下降树的高度

范围查询B+树叶子节点保存了指向下一个叶子节点的指针,B+树全部叶结点构成一个有序链表,在查询中范围查询很常见,B树范围查询效率低;

遍历B+树叶子节点就能得到全部的记录,至关于整棵树的记录遍历

 

 

二叉树、平衡树、红黑树 

平衡树是为了解决二叉查找树退化为链表的状况,而红黑树是为了解决平衡树在插入、删除等操做须要频繁调整

https://mp.weixin.qq.com/s/t1J2HnAhzrks-6AD4s8JYw

TODO待写

 

链表转红黑树、红黑树的插入与删除等没有详细说明。

ConcurrentHashMap中红黑树相关:https://blog.csdn.net/chenssy/article/details/73749297

红黑树30张图详解!!!

 

来源:

http://www.javashuo.com/article/p-mtzfapmv-a.html

http://www.importnew.com/20386.html

浅析几种线程安全模型-importNew

 https://blog.csdn.net/daiyuhe/article/details/89424736

相关文章
相关标签/搜索