前言

集合与数组一样,也是一个容器(可以存储任意数量的具有共同属性的对象),与数组不同的是,集合的长度不定,可以无限的向集合中添加元素,而且集合中存储的元素类型可以随意。

集合框架包含:接口抽象类以及完全定义的类,其中,每个完全定义的类继承了一个或多个抽象类,并实现了一个或多个接口。

接口:在JAVA编程语言中是一个抽象类型,是抽象方法的集合,接口通常以interface来声明。一个类通过继承接口的方式,从而来继承接口的抽象方法。

抽象类:具体的实现,重用性很高的数据结构,在一个实现了某个集合框架中的接口的对象身上完成某种有用的,可复用的计算的方法,比如排序,查找,统计等。

完全定义的类:具备某些共同特征的实体的集合。

应用集合框架的优点是和我们设计开发中的公共类同一思想,通过这些在无关API之间的简易的互用性,使你免除了为改编对象或转换代码以便联合这些API而去写大量的代码。它提高了程序速度和质量。

本文提纲



用java如何判断两个集合的内容相同 java判断两个集合是否相等_判断集合相等

正文


Collection接口



用java如何判断两个集合的内容相同 java判断两个集合是否相等_数据_02

List


是什么?


List:

列表, 元素是有序的(元素 带角标索引 ), 可以有重复元素,可以有null元素。

ArrayList:底层的数据结构使用的是数组结构,Object数组。特点:查询速度很快。但是增删稍慢。线程不同步。


|-- 因为底层采用数组的数据结构,而数组中的元素在堆内存中是连续分配的,而且有索引,所以查询快,增删稍慢。 |-- 在使用迭代器遍历元素时,不能再使用集合的方法操作集合中的元素,不能增加或删除个数。 |-- 调用集合的contains()或remove()方法时底层会调用equals方法,如果存入集合的对象没有实现equals(),则调用Object的equals()方法。

LinkedList:底层使用的链表数据结构,双向循环链表。特点:增删速度很快,查询稍慢。线程不同步。



Vector:底层是数组数据结构,线程同步。被ArrayList替代了,因为效率低。


总结:

在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。

List遍历的最佳实践


 1、传统的for循环遍历,基于计数器的:         遍历者自己在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后,停止。主要就是需要按元素的位置来读取元素。 2、迭代器遍历,Iterator:         每一个具体实现的数据集合,一般都需要提供相应的Iterator。相比于传统for循环,Iterator取缔了显式的遍历计数器。所以基于顺序存储集合的Iterator可以直接按位置访问数据。而基于链式存储集合的Iterator,正常的实现,都是需要保存当前遍历的位置。然后根据当前位置来向前或者向后移动指针。 3、foreach循环遍历:         根据反编译的字节码可以发现,foreach内部也是采用了Iterator的方式实现,只不过Java编译器帮我们生成了这些代码。

多线程场景下如何使用ArrayList?


ArrayList 不是线程安全的,如果遇到多线程场景,最常用的方法是通过 Collections 的 synchronizedList 方法将 ArrayList 转换成线程安全的容器后再使用。

List list =Collections.synchronizedList(new ArrayList);list.add("test");
List list =Collections.synchronizedList(new ArrayList);
list.add("test");


Set


HashSet实现(底层)原理


HashSet实现Set接口,由 哈希表 (实际上是一个HashMap实例)支持。 它不保证set 的迭代顺序;特别是它不保证该顺序恒久不变。 此类允许使用null元素。HashSet的存储方式是把HashMap中的Key作为Set的对应存储项。该行的value就是一个Object类型的常量。

(HashMap储存键值对 HashSet仅仅存储对象)

为什么无重复?


HashSet的存储方式是把HashMap中的Key作为Set的对应存储项。因为HashMap的key是不能有重复的。

HashSet的实现: 对于HashSet而言,它是基于HashMap实现的,HashSet底层使用HashMap来保存所有元素,因此HashSet 的实现比较简单,相关HashSet的操作,基本上都是直接调用底层HashMap的相关方法来完成, (实际底层会初始化一个空的HashMap,并使用默认初始容量为16和加载因子0.75。) 对于HashSet中保存的对象,请注意正确重写其equals和hashCode方法,以保证放入的对象的唯一性。

一定要覆盖自定义类的 equals 和 hashCode 方法,hashCode 方法是找到当前对象在 Node 数组的位置,而 equals 是比较当前对象与对应坐标链表中的对象是否相同。

插入

当有新值加入时,底层的HashMap会判断Key值是否存在(HashMap细节请移步深入理解HashMap),如果不存在,则插入新值,同时这个插入的细节会依照HashMap插入细节;如果存在就不插入。

List和Set区别

               List

           Set

特点

一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。

一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。

遍历

 List 支持for循环,也就是通过下标来遍历,也可以用迭代器。

set只能用迭代,因为他无序,无法用下标来取得想要的值。

和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变

检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。

hashCode()与equals()的相关规定:

1.如果两个对象相等,则hashcode一定也是相同的 2.两个对象相等,对两个equals方法返回true 3.两个对象有相同的hashcode值,它们也不一定是相等的 4.综上,equals方法被覆盖过,则hashCode方法也必须被覆盖 5.hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。 ==与equals的区别

1.==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同 2.==是指对内存地址进行比较 equals()是对字符串的内容进行比较3.==指引用是否相同 equals()指的是值是否相同

Map接口



用java如何判断两个集合的内容相同 java判断两个集合是否相等_链表_03

Hash(散列函数)


什么是哈希冲突?当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做 碰撞(哈希碰撞)。 解决哈希冲突

JDK1.8之前


关键字列表为(19,14,23,01,68,20,84,27,55,11,10,79),则哈希函数为H(key)=key MOD 13。则采用 除留余数法 和 链地址法 后得到的预想结果应该为:

用java如何判断两个集合的内容相同 java判断两个集合是否相等_数组_04

JDK1.8之后


相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。

用java如何判断两个集合的内容相同 java判断两个集合是否相等_数据_05

HashMap实现(底层)原理


 HashMap概述:

HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

HashMap的数据结构: HashMap实际上是一个“数组+链表+红黑树”的数据结构.(JDK1.8后加入了红黑树) HashMap的存取实现:

JDK1.8之前



当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值,根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。


用java如何判断两个集合的内容相同 java判断两个集合是否相等_数据_06

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,         boolean evict) {  HashMap.Node[] tab;  HashMap.Node p;    int n, i;  // 1.如果table为空或者长度为0,即没有元素,那么使用resize()方法扩容     if ((tab = table) == null || (n = tab.length) == 0)       n = (tab = resize()).length;    // 2.计算插入存储的数组索引i,此处计算方法同 1.7 中的indexFor()方法     // 如果数组为空,即不存在Hash冲突,则直接插入数组         if ((p = tab[i = (n - 1) & hash]) == null)           tab[i] = newNode(hash, key, value, null);     // 3.插入时,如果发生Hash冲突,则依次往下判断              else {    HashMap.Node e; K k;       // a.判断table[i]的元素的key是否与需要插入的key一样,若相同则直接用新的value覆盖掉旧的value        // 判断原则equals() - 所以需要当key的对象重写该方法                  if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))      e = p;        // b.继续判断:需要插入的数据结构是红黑树还是链表         // 如果是红黑树,则直接在树中插入 or 更新键值对                        else if (p instanceof HashMap.TreeNode)                           e = ((HashMap.TreeNode)p).putTreeVal(this, tab, hash, key, value);         // 如果是链表,则在链表中插入 or 更新键值对                               else {            // i .遍历table[i],判断key是否已存在:采用equals对比当前遍历结点的key与需要插入数据的key             //    如果存在相同的,则直接覆盖               // ii.遍历完毕后任务发现上述情况,则直接在链表尾部插入数据      //    插入完成后判断链表长度是否 > 8:若是,则把链表转换成红黑树                                         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;      }    }    // 对于i 情况的后续操作:发现key已存在,直接用新value覆盖旧value&返回旧value                                                                   if (e != null) { // existing mapping for key                                V oldValue = e.value;                                                                       if (!onlyIfAbsent || oldValue == null)                                  e.value = value;      afterNodeAccess(e);                                                                          return oldValue;    }  }                            ++modCount;  // 插入成功后,判断实际存在的键值对数量size > 最大容量  // 如果大于则进行扩容                                                                          if (++size > threshold)    resize();  // 插入成功时会调用的方法(默认实现为空)                                                                           afterNodeInsertion(evict);  return null;}

JDK1.8之后


put(): 根据key计算得到key.hash = (h = k.hashCode()) ^ (h >>> 16); 根据key.hash计算得到桶数组的索引index = key.hash & (table.length - 1),这样就找到该key的存放位置了: ① 如果该位置没有数据,用该数据新生成一个节点保存新数据,返回null; ② 如果该位置有数据是一个红黑树,那么执行相应的插入 / 更新操作。 ③ 如果该位置有数据是一个链表,分两种情况一是该链表没有这个节点,另一个是该链表上有这个节点,注意这里判断的依据是key.hash是否一样:如果该链表没有这个节点,那么采用尾插法新增节点保存新数据,返回null;如果该链表已经有这个节点了,那么找到该节点并更新新数据,返回老数据。 get(): 计算需获取数据的hash值(计算过程跟put一样),计算存放在数组table中的位置(计算过程跟put一样),然后依次在数组,红黑树,链表中查找(通过equals()判断),最后再判断获取的数据是否为空,若为空返回null否则返回该数据。 扩容机制的实现

扩容(resize)就是重新计算容量。当向HashMap对象里不停的添加元素,而HashMap对象内部的桶数组无法装载更多的元素时,HashMap对象就需要扩大桶数组的长度,以便能装入更多的元素。 capacity 就是数组的长度/大小,loadFactor 是这个数组填满程度的最大比比例。 size表示当前HashMap中已经储存的Node的数量,包括桶数组和链表 / 红黑树中的的Node。 threshold表示扩容的临界值,如果size大于这个值,则必须调用resize()方法进行扩容。 在jdk1.7及以前,threshold = capacity * loadFactor,其中 capacity 为桶数组的长度。这里需要说明一点,默认负载因子0.75是是对空间和时间(纵向横向)效率的一个平衡选择,建议大家不要修改。jdk1.8对threshold值进行了改进,通过一系列位移操作算法最后得到一个power of two size的值 什么时候扩容

当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值—即当前数组的长度乘以加载因子的值的时候,就要自动扩容啦。 扩容必须满足两个条件:

1、 存放新值的时候 当前已有元素的个数 (size) 必须大于等于阈值 2、 存放新值的时候当前存放数据发生hash碰撞(当前key计算的hash值换算出来的数组下标位置已经存在值) 如果计算的哈希位置有值(及hash冲突),且key值一样,则覆盖原值value,并返回原值value。

if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {        V oldValue = e.value;        e.value = value;        e.recordAccess(this);        return oldValue;      }
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
        V oldValue = e.value;
        e.value = value;
        e.recordAccess(this);
        return oldValue;
      }


resize()方法: 该函数有2种使用情况1.初始化哈希表 2.当前数组容量过小,需扩容

过程:

HashMap是先插入数据再进行扩容的,但是如果是刚刚初始化容器的时候是先扩容再插入数据。

插入键值对时发现容量不足,调用resize()方法方法,

1.首先进行异常情况的判断,如是否需要初始化,二是若当前容量》最大值则不扩容,

2.然后根据新容量(是就容量的2倍、)新建数组,将旧数组上的数据(键值对)转移到新的数组中,这里包括:(遍历旧数组的每个元素,重新计算每个数据在数组中的存放位置。(原位置或者原位置+旧容量),将旧数组上的每个数据逐个转移到新数组中,这里采用的是尾插法。)

3.新数组table引用到HashMap的table属性上

4.最后重新设置扩容阙值,


Hashtable底层原理


概念: HashTable类继承自Dictionary类, 实现了Map接口。大部分的操作都是通过synchronized锁保护的,是线程安全的, key、value都不可以为null, 每次put方法不允许null值,如果发现是null,则直接抛出异常。 官方文档也说了:如果在非线程安全的情况下使用,建议使用HashMap替换,如果在线程安全的情况下使用,建议使用ConcurrentHashMap替换。 数据结构: 数组+链表。 存取实现: put(): 限制了value不能为null。 由于直接使用key.hashcode(),而没有向hashmap一样先判断key是否为null,所以key为null时,调用key.hashcode()会出错,所以hashtable中key也不能为null。 Hashtable是在链表的头部添加元素的。 int index = (hash & 0x7FFFFFFF) %tab.length; 获取index的方式与HashMap不同 扩容机制: Hashtable默认capacity是11,默认负载因子是0.75.。当前表中的Entry数量,如果超过了阈值,就会扩容,即调用rehash方法,重新计算每个键值对的hashCode; 判断新的容量是否超过了上限,没超过就新建一个新数组,大小为原数组的2倍+1,将旧数的键值对重新hash添加到新数组中。

HashMap 与 HashTable 有什么区别

HashMap

HashTable

线程安全

非线程安全

线程安全,内部的方法基本都经过 synchronized 修饰

效率

基本淘汰

对null键值

null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null

put 的键值只要有一个 null,直接抛NullPointerException。

初始值扩容

默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。

 会将其扩充为2的幂次方大小

 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。会直接使用你给定的大小

底层DS

当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间

总结:

ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同步,HashTable 考虑了同步的问题。但是 HashTable 在每次同步执行时都要锁住整个结构。ConcurrentHashMap 锁的方式是稍微 细粒度 的。 对null key和null value支持 :

Hashtable 是不允许键或值为 null ,Ha shtable使用的是安全失败机制(fail-safe),这种机制会使你此次读到的数据不一定是最新的数据. fail-fast(快速失败机制):

机制:

是java集合的一种 错误检测 机制,当多个线程对集合进行结构上的改变的操作时(增加,删除等),有可能会产生 fail-fast 机制。 原理:

迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。每当迭代器使用 hashNext()/next() 遍历下一个元素之前,会先比较这两个值是否相等,此时他们不相等,就抛出了 ConcurrentModificationException. ,终止遍历。 解决办法:


1.在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。

2.使用CopyOnWriteArrayList来替换ArrayList



ConcurrentHashMap 的工作原理


概念: ConcurrentHashMap的目标是实现支持高并发、高吞吐量的线程安全的HashMap。

JDK1.8之前:


数据结构: ConcurrentHashMap是由Segment数组结构和 多个HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色, HashEntry 用来封装散列映射表中的键值对。在 HashEntry 类中,key,hash和 next 域都被声明为 final 型,value 域被声明为 volatile 型。

winter

volatile:

1.保证内存可见性(指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果,另一个线程马上就能看到。)

当对非volatile变量进行读写的时候,每个线程先从主内存拷贝变量到CPU缓存中,如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的CPU cache中。

volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,保证了每次读写变量都从主内存中读,跳过CPU cache这一步。当一个线程修改了这个变量的值,新值对于其他线程是立即得知的。

2 禁止指令重排(关于volatile特性,将会在后边文章展开)



用java如何判断两个集合的内容相同 java判断两个集合是否相等_判断集合相等_07

用java如何判断两个集合的内容相同 java判断两个集合是否相等_数组_08


一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap相似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。

static final class Segment<K,V> extends ReentrantLock implements Serializable {    private static final long serialVersionUID = 2249069246763182397L;    // 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶    transient volatile HashEntry[] table;    transient int count;        // 快速失败(fail—fast)    transient int modCount;        // 大小    transient int threshold;        // 负载因子    final float loadFactor;}


put和get的时候,都是现根据key.hashCode()算出放到哪个Segment中:ConcurrentHashMap中默认是把segments初始化为长度为16的数组

JDK1.8后:

变化:

ConcurrentHashMap的JDK8与JDK7版本的并发实现相比,最大的区别在于JDK8的锁粒度更细,理想情况下talbe数组元素的大小就是其支持并发的最大个数

实现:

改进一:取消segments字段,直接采用transient volatile HashEntry[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。

数据结构:

改进二:将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。对于hash表来说,最核心的能力在于将key hash之后能均匀的分布在数组中。

概念:

JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本。

用java如何判断两个集合的内容相同 java判断两个集合是否相等_数组_09

总结:Node+CAS + synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。

一些成员:

Node是ConcurrentHashMap存储结构的基本单元,继承于HashMap中的Entry,用于存储数据。,就是一个链表,但是只允许对数据进行查找,不允许进行修改

通过TreeNode作为存储结构代替Node来转换成黑红树。

TreeBin就是封装TreeNode的容器,它提供转换黑红树的一些条件和锁的控制

// 读写锁状态

static final int WRITER = 1; // 获取写锁的状态

static final int WAITER = 2; // 等待写锁的状态

static final int READER = 4; // 增加数据时读锁的状态

构造器

public ConcurrentHashMap() {

} 初始化其实是一个空实现, 初始化操作并不是在构造函数实现的,而是在put操作中实现。还提供了其他的构造函数,有指定容量大小或者指定负载因子,跟HashMap一样。

存取实现:

put():对当前的table进行无条件自循环直到put成功

新:

版本1:

如果没有初始化就先调用initTable()方法来进行初始化过程

如果没有hash冲突就直接CAS插入

如果还在进行扩容操作就先进行扩容(ForwardingNode的hash值判断)

如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入,

最后一个如果该链表的数量大于阈值8,就要先转换成黑红树的结构,break再一次进入循环(树化)

如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容。

版本2:

1,判空;ConcurrentHashMap的key、value都不允许为null

2,计算hash。利用方法计算hash值。

3,遍历table,进行节点插入操作,过程如下:

如果table为空,则表示ConcurrentHashMap还没有初始化,则进行初始化操作:initTable()

根据hash值获取节点的位置i,若该位置为空,则直接插入,这个过程是不需要加锁的。计算f位置:i=(n - 1) & hash

如果检测到fh = f.hash == -1,则f是ForwardingNode节点,表示有其他线程正在进行扩容操作,则帮助线程一起进行扩容操作

如果f.hash >= 0 表示是链表结构,则遍历链表,如果存在当前key节点则替换value,否则插入到链表尾部。如果f是TreeBin类型节点,则按照红黑树的方法更新或者增加节点

若链表长度 > TREEIFY_THRESHOLD(默认是8),则将链表转换为红黑树结构

4,调用addCount方法,ConcurrentHashMap的size + 1

如果一个或多个线程正在对ConcurrentHashMap进行扩容操作,当前线程也要进入扩容的操作中。这个扩容的操作之所以能被检测到,是因为transfer方法中在空结点上插入forward节点,如果检测到需要插入的位置被forward节点占有,就帮助进行扩容;

如果检测到要插入的节点是非空且不是forward节点,就对这个节点加锁,这样就保证了线程安全。尽管这个有一些影响效率,但是还是会比hashTable的synchronized要好得多。

get()

计算hash值,定位到该table索引位置,如果是首节点符合就返回

如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回。

以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null

概括版:

对于get读操作,如果当前节点有数据,还没迁移完成,此时不影响读,能够正常进行。

如果当前链表已经迁移完成,那么头节点会被设置成fwd节点,get线程会帮助扩容。

对于put/remove写操作,如果当前链表已经迁移完成,那么头节点会被设置成fwd节点,此时写线程会帮助扩容,如果扩容没有完成,当前链表的头节点会被锁住,所以写线程会被阻塞,直到扩容完成。

扩容机制:

总体:

计算每个线程可以处理的桶区间。默认 16.

初始化临时变量 nextTable,扩容 2 倍。

死循环,计算下标。完成总体判断。

如果桶内有数据,同步转移数据。通常会像链表拆成 2 份。

先来看一下单线程是如何完成的:它的大体思想就是遍历、复制的过程。首先根据运算得到需要遍历的次数i,然后利用tabAt方法获得i位置的元素:如果这个位置为空,就在原table中的i位置放入forwardNode节点,这个也是触发并发.

扩容的关键点;

如果这个位置是Node节点(fh>=0),如果它是一个链表的头节点,就构造一个反序链表,把他们分别放在nextTable的i和i+n的位置上

如果这个位置是TreeBin节点(fh<0),也做一个反序处理,并且判断是否需要untreefi,把处理的结果分别放在nextTable的i和i+n的位置上

遍历过所有的节点以后就完成了复制工作,这时让nextTable作为新的table,并且更新sizeCtl为新容量的0.75倍 ,完成扩容。

多线程是如何完成的:

如果遍历到的节点是forward节点,就向后继续遍历,再加上给节点上锁的机制,就完成了多线程的控制。多线程遍历节点,处理了一个节点,就把对应点的值set为forward,另一个线程看到forward,就向后遍历。这样交叉就完成了复制工作。