集合概要小结

集合概要小结,第1张

集合概要小结 一.简介

集合的主要作用是存储对象的容器,本质是用于存储对象的数据结构。

二.类型

集合类存放于java.util包中,主要set、list、map。

  • Collection是集合list set queue的最基本接口
  • Iterator:迭代器,可以通过迭代器遍历集合中的数据
  • Map:映射表的基础接口
    关系如下图:

2.1 List
一共三个实现类:ArrayList、Vector、linkedList

ArryList
查询较快,增删较慢。内部通过数组实现,允许对元素进行快速随机访问。数组的每个元素之间不能有间隔,当数组大小不能满足时需要增加存储能力,就要将已有数组的数据复制到新的存储空间中。

Vector
也是通过数组实现,支持同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性。

linkList
用链表结构存储数据,增删较快,查询较慢,可以当作堆栈、队列和双向队列使用

2.2 Set
无序,值不重复,如果想要让两个不同的对象视为相等的,必须覆盖Object的hashCode方法和equals方法

HashSet
哈希表存放的是哈希值。HashSet 存储元素的顺序并不是按照存入时的顺序(和 List 显然不 同) 而是按照哈希值来存的所以取数据也是按照哈希值取得。元素的哈希值是通过元素的 hashcode 方法来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较 equals 方法 如果 equls 结果为 true ,HashSet 就视为同一个元素。如果 equals 为 false 就不是同一个元素。

TreeSet

  • TreeSet()是使用二叉树的原理对新 add()的对象按照指定的顺序排序(升序、降序),每增 加一个对象都会进行排序,将对象插入的二叉树指定的位置;
  • Integer 和 String 对象都可以进行默认的 TreeSet 排序,而自定义类的对象是不可以的,自己定义的类必须实现 Comparable 接口,并且覆写相应的 compareTo()函数,才可以正常使用;
  • 在覆写 compare()函数时,要返回相应的值才能使 TreeSet 按照一定的规则来排序;
  • 比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。

linkHashSet

linkedHashSet底层使用 linkedHashMap 来保存所有元素,它继承于 HashSet

2.3 Map

HashMap
HashMap 根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap 最多只允许一条记录的键为 null,允许多条记录的值为 null。HashMap 非线程安全,即任一时刻可以有多个线程同时写 HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以使用ConcurrentHashMap。
java7和java8的区别
根据 Java7 HashMap 的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)。为了降低这部分的开销,在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。

HashTable
遗留类,建议不使用,不需要线程安全的场合可以使用HashMap,需要线程安全的场合可以用ConcurrentHashMap

TreeMap
TreeMap 实现 SortedMap 接口,能够把它保存的记录根据键排序,默认是按键值的升序排序.在使用 TreeMap 时,key 必须实现 Comparable 接口或者在构造 TreeMap 传入自定义的Comparator,否则会在运行时抛出 java.lang.ClassCastException 类型的异常。

linkHashMap
有序

DEFAULT_INITIAL_CAPACITY =16:默认初始容量
MAXIMUM_CAPACITY :最大容量1 << 30
DEFAULT_LOAD_FACTOR =0.75f:负载因子
TREEIFY_THRESHOLD =8:阈值 如果链表长度超过阀值就把链表转成红黑树
UNTREEIFY_THRESHOLD =6:链表长度低于6,就把红黑树转回链表
MIN_TREEIFY_CAPACITY =64:开始转换为树结构的值

三. 扩容机制
如果这个桶中bin的数量小于 TREEIFY_THRESHOLD 当然不会转化成树形结构存储;
如果这个桶中bin的数量大于了 TREEIFY_THRESHOLD ,但是capacity小于 MIN_TREEIFY_CAPACITY 则
依然使用链表结构进行存储,此时会对HashMap进行扩容;
如果capacity大于了 MIN_TREEIFY_CAPACITY ,则会进行树化。

HashMap
在多线程环境下,使用HashMap进行put *** 作会引起死循环,导致CPU利用率接近100%,所以在并发环境下不能使用HashMap。
jdk1.7采用头插法
HashMap的线程不安全主要是发生在扩容函数中,即根源在transfer函数中

void transfer(Entry[] newTable, boolean rehash) { 
    int newCapacity = newTable.length; 
    for (Entry e : table) { 
        while(null != e) { 
            Entry next = e.next; 
            if (rehash) { 
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity); 
            e.next = newTable[i]; 
            newTable[i] = e; 
            e = next; 
        } 
    } 
}

这段代码是 HashMap 的扩容 *** 作,重新定位每个桶的下标,并采用头插法将元素迁移到新数组中。头插法会将链表的顺序翻转,这也是形成死循环的关键点。理解了头插法后再继续往下看是如何造成死循环以及数据丢失的。
扩容造成死循环和数据丢失的分析过程
假设有两个线程A、B同时对下面这个HashMap进行扩容 *** 作

正常扩容后的结果是下面这样子的:

但是当线程A执行到上面 transfer 函数的第11行代码时,线程A被挂起。即如下图中位置所示:
newTable[i] = e; //线程在此处挂起
此时线程A中:e=3、next=7、e.next=null

当线程A的时间片耗尽后,CPU开始执行线程B,并在线程B中成功的完成了数据迁移

重点来了,根据Java内存模式可知,线程B执行完数据迁移后,此时主内存中 newTable 和 table 都是最新的,也就是说:7.next=3、3.next=null。
随后线程A获得CPU时间片继续执行 newTable[i] = e ,将3放入新数组对应的位置,执行完此轮循环后线程A的情况如下:

接着继续执行下一轮循环,此时e=7,从主内存中读取e.next时发现主内存中7.next=3,于是乎next=3,并将7采用头插法的方式放入新数组中,并继续执行完此轮循环,结果如下:

执行下一次循环可以发现,next=e.next=null,所以此轮循环将会是最后一轮循环。接下来当执行完e.next=newTable[i]即3.next=7后,3和7之间就相互连接了,当执行完newTable[i]=e后,3被头插法重新插入到链表中,执行结果如下图所示:

上面说了此时e.next=null即next=null,当执行完e=null后,将不会进行下一轮循环。到此线程A、B的扩容 *** 作完成,很明显当线程A执行完后, HashMap 中出现了环形结构,当在以后对该 HashMap 进行 *** 作时会出现死循环。并且从上图可以发现,元素5在扩容期间被莫名的丢失了,这就发生了数据丢失的问题。
JDK1.8不安全分析(尾插法)
根据上面JDK1.7出现的问题,在JDK1.8中已经得到了很好的解决,如果你去阅读1.8的源码会发现找不到 transfer 函数,因为JDK1.8直接在 resize 函数中完成了数据迁移。另外说一句,JDK1.8在进行元素插入时使用的是尾插法。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { 
    Node[] tab; Node p; 
    int n, i; 
    if ((tab = table) == null || (n = tab.length) == 0) 
        n = (tab = resize()).length; 
    if ((p = tab[i = (n - 1) & hash]) == null) // 如果没有hash碰撞则直接插入元素 
        tab[i] = newNode(hash, key, value, null); 
    else {
        Node e; K k; 
          if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) 
              e = p; 
          else if (p instanceof TreeNode) 
              e = ((TreeNode)p).putTreeval(this, tab, hash, key, value); 
          else {
              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; 
              } 
          }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; 
}

其中第六行代码是判断是否出现hash碰撞,假设两个线程A、B都在进行put *** 作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。
除此之前,还有就是代码的第38行处有个 ++size ,我们这样想,还是线程A、B,这两个线程同时进行put *** 作时,假设当前 HashMap 的zise大小为10,当线程A执行到第38行代码时,从主内存中获得size的值为10后准备进行+1 *** 作,但是由于时间片耗尽只好让出CPU,线程B快乐的拿到CPU还是从主内存中拿到size的值10进行+1 *** 作,完成了put *** 作并将size=11写回主内存,然后线程A再次拿到CPU并继续执行(此时size的值仍为10),当执行完put *** 作后,还是将size=11写回内存,此时,线程A、B都执行了一次put *** 作,但是size的值只增加了1,所有说还是由于数据覆盖又导致了线程不安全。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5612245.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-15
下一篇 2022-12-15

发表评论

登录后才能评论

评论列表(0条)

保存