Java Web基础篇之Java集合类

1、集合总体框架

Java集合总体框架

摘自:
Java 集合系列01之 总体框架


2、ArrayList与LinkedList区别

大致区别:

  1. ArrayList是实现了基于动态数组的数据结构(初始容量为0,第一次添加元素时初始为10,扩容时采用Arrays.copyOf(),空间不足时扩容为之前容量的1.5倍),LinkedList是基于双向链表结构(不考虑扩容等问题,直接添加元素即可)。
  2. 对于随机访问的get和set方法,ArrayList要优于LinkedList,因为LinkedList要移动指针。
  3. 对于新增和删除操作add和remove,LinkedList比较占优势,因为ArrayList要移动数据。

性能上的优缺点:

1.对ArrayList和LinkedList而言,在列表末尾增加一个元素所花的开销都是固定的。对 ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对LinkedList而言,这个开销是统一的,分配一个内部Entry对象。
2.在ArrayList集合中添加或者删除一个元素时,当前的列表所所有的元素都会被移动。而LinkedList集合中添加或者删除一个元素的开销是固定的。
3.LinkedList集合不支持高效的随机随机访问(RandomAccess),因为可能产生二次项的行为。
4.ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间

相关:
ArrayList详细介绍(JDK1.6源码及使用)
LinkedList详细介绍(JDK1.6源码及使用)
Java 集合系列08之 List总结(LinkedList, ArrayList等使用场景和性能分析)
ArrayList源码解析(JDK8)
LinkedList源码解析(JDK8)
CopyOnArrayList介绍
CopyOnArrayList与Collections.synchronizedList的性能对比


3、HashTable、HashMap、TreeMap区别

HashMap的数据结构

HashMap的数据结构
摘自:
一文读懂HashMap

大致区别:

 HashMap 是基于“拉链法”实现的散列表,JDK1.8后为链表+红黑树。一般用于单线程程序中。
 Hashtable 也是基于“拉链法”实现的散列表,JDK1.8后为链表+红黑树。它一般用于多线程程序中。
 TreeMap 是有序的散列表,它是通过红黑树实现的。它一般用于单线程中存储有序的映射。
 WeakHashMap 也是基于“拉链法”实现的散列表,JDK1.8后为链表+红黑树,它一般也用于单线程程序中。相比HashMap,WeakHashMap中的键是“弱键”,当“弱键”被GC回收时,它对应的键值对也会被从WeakHashMap中删除;而HashMap中的键是强键。

HashMap和Hashtable的不同点:

1、 继承和实现方式不同。

HashMap和Hashtable都实现了Map、Cloneable、java.io.Serializable接口,但是HashMap继承于AbstractMap,而Hashtable继承于Dictionary

2、线程安全不同。

Hashtable的几乎所有函数都是同步的,即它是线程安全的,支持多线程。而HashMap的函数则是非同步的,它不是线程安全的。若要在多线程中使用HashMap,需要我们额外的进行同步处理。 对HashMap的同步处理可以使用Collections类提供的synchronizedMap静态方法,或者直接使用JDK 5.0之后提供的java.util.concurrent包里的ConcurrentHashMap类。

3、对null值的处理不同

HashMap的key、value都可以为null。
Hashtable的key、value都不可以为null。

4、支持的遍历种类不同

HashMap只支持Iterator(迭代器)遍历。
而Hashtable支持Iterator(迭代器)和Enumeration(枚举器)两种方式遍历。

5、通过Iterator迭代器遍历时,遍历的顺序不同

HashMap是“从前向后”的遍历数组;再对数组具体某一项对应的链表,从表头开始进行遍历。
Hashtabl是“从后往前”的遍历数组;再对数组具体某一项对应的链表,从表头开始进行遍历。

6、容量的初始值 和 增加方式都不一样

HashMap默认的容量大小是16;增加容量时,每次将容量变为“原始容量x2”。
Hashtable默认的容量大小是11;增加容量时,每次将容量变为“原始容量x2 + 1”。
HashMap默认的“加载因子”是0.75, 默认的容量大小是16。
Hashtable默认的“加载因子”是0.75, 默认的容量大小是11。

7、 添加key-value时的hash值算法不同

HashMap添加元素时,是使用自定义的哈希算法。
Hashtable没有自定义哈希算法,而直接采用的key的hashCode()。

8、 部分API不同

Hashtable支持contains(Object value)方法,而且重写了toString()方法;
而HashMap不支持contains(Object value)方法,没有重写toString()方法。

相关:
Map总结(HashMap, Hashtable, TreeMap, WeakHashMap等使用场景)
Hashtable详细介绍(JDK1.6源码解析)和使用示例
HashMap详细介绍(JDK1.6源码解析)和使用示例
TreeMap详细介绍(JDK1.6源码解析)和使用示例
HashMap实现原理
HashMap源码解析(JDK8)
LinkedHashMap源码解析(JDK8)
HashMap链表插入是头部还是尾部
红黑树介绍
二叉树、红黑树、B树性能比较
为什么HashMap用红黑树
“JUC集合”04之 ConcurrentHashMap
ConcurrentHashMap原理分析
理解putIfAbsent
Java的反射原理


4、ConcurrentHashMap分析

4.1、结构分析:

ConcurrentHashMap 类中包含两个静态内部类 HashEntry 和 Segment。HashEntry 用来封装映射表的键 / 值对;Segment 用来充当锁的角色,每个 Segment 对象守护整个散列映射表的若干个桶。每个桶是由若干个 HashEntry 对象链接起来的链表。一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组。

进一步分析:
Segment ( 默认初始16 个 Segment 对象)类继承于 ReentrantLock 类,从而使得 Segment 对象能充当锁的角色。每个 Segment 对象用来守护其(成员对象 table 中)包含的若干个桶。
table 是一个由 HashEntry 对象组成的数组。table 数组的每一个数组成员就是散列映射表的一个桶。
count 变量是一个计数器,它表示每个 Segment 对象管理的 table 数组(若干个 HashEntry 组成的链表)包含的 HashEntry 对象的个数。每一个 Segment 对象都有一个 count 对象来表示本 Segment 中包含的 HashEntry 对象的总数。注意,之所以在每个 Segment 对象中包含一个计数器,而不是在ConcurrentHashMap 中使用全局的计数器,是为了避免出现“热点域”而影响 ConcurrentHashMap 的并发性。

4.2、用分离锁实现多个线程间的并发写操作

在 ConcurrentHashMap 中,线程对映射表做读操作时,一般情况下不需要加锁就可以完成,对容器做结构性修改的操作才需要加锁。注意:这里的加锁操作是针对(键的 hash 值对应的)某个具体的 Segment,锁定的是该 Segment 而不是整个 ConcurrentHashMap。因为插入键 / 值对操作只是在这个 Segment 包含的某个桶中完成,不需要锁定整个ConcurrentHashMap。此时,其他写线程对另外 15 个Segment 的加锁并不会因为当前线程对这个 Segment 的加锁而阻塞。同时,所有读线程几乎不会因本线程的加锁而阻塞(除非读线程刚好读到这个 Segment 中某个 HashEntry 的 value 域的值为 null,此时需要加锁后重新读取该值)。
相比较于 HashTable 和由同步包装器包装的 HashMap每次只能有一个线程执行读或写操作,ConcurrentHashMap 在并发访问性能上有了质的提高。在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设置为 16),及任意数量线程的读操作。

4.3、用 HashEntry 对象的不变性来降低读操作对加锁的需求

在 ConcurrentHashMap 中,不允许用 null 作为键和值,当读线程读到某个 HashEntry 的 value 域的值为 null 时,便知道产生了冲突——发生了重排序现象,需要加锁后重新读入这个 value 值。

4.4、用 Volatile 变量协调读写线程间的内存可见性

读线程在读取散列表时,基本不需要加锁就能成功获得需要的值。这两个特性相配合,不仅减少了请求同一个锁的频率(读操作一般不需要加锁就能够成功获得值),也减少了持有同一个锁的时间(只有读到 value 域的值为 null 时 , 读线程才需要加锁后重读)。

4.5、ConcurrentHashMap 实现高并发的总结

基于通常情形而优化
在实际的应用中,散列表一般的应用场景是:除了少数插入操作和删除操作外,绝大多数都是读取操作,而且读操作在大多数时候都是成功的。正是基于这个前提,ConcurrentHashMap 针对读操作做了大量的优化。通过 HashEntry 对象的不变性和用 volatile 型变量协调线程间的内存可见性,使得 大多数时候,读操作不需要加锁就可以正确获得值。这个特性使得 ConcurrentHashMap 的并发性能在分离锁的基础上又有了近一步的提高。

4.6、总结

ConcurrentHashMap 是一个并发散列映射表的实现,它允许完全并发的读取,并且支持给定数量的并发更新。相比于 HashTable 和用同步包装器包装的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 拥有更高的并发性。在 HashTable 和由同步包装器包装的 HashMap 中,使用一个全局的锁来同步不同线程间的并发访问。同一时间点,只能有一个线程持有锁,也就是说在同一时间点,只能有一个线程能访问容器。这虽然保证多线程间的安全并发访问,但同时也导致对容器的访问变成串行化的了。
在使用锁来协调多线程间并发访问的模式下,减小对锁的竞争可以有效提高并发性。有两种方式可以减小对锁的竞争:

  1. 减小请求 同一个锁的 频率。
  2. 减少持有锁的 时间。
    ConcurrentHashMap 的高并发性主要来自于三个方面:
  3. 用分离锁实现多个线程间的更深层次的共享访问。
  4. 用 HashEntry 对象的不变性来降低执行读操作的线程在遍历链表期间对加锁的需求。
  5. 通过对同一个 Volatile 变量的写 / 读访问,协调不同线程间读 / 写操作的内存可见性。
    使用分离锁,减小了请求 同一个锁的频率。
    通过 HashEntery 对象的不变性及对同一个 Volatile 变量的读 / 写来协调内存可见性,使得 读操作大多数时候不需要加锁就能成功获取到需要的值。由于散列映射表在实际应用中大多数操作都是成功的 读操作,所以 2 和 3 既可以减少请求同一个锁的频率,也可以有效减少持有锁的时间。
    通过减小请求同一个锁的频率和尽量减少持有锁的时间 ,使得 ConcurrentHashMap 的并发性相对于 HashTable 和用同步包装器包装的 HashMap有了质的提高。

摘自:
ConcurrentHashMap 实现高并发的总结


5、HashMap(线程安全性及高并发讨论)

1、hash冲突时,两个线程同时修改链表或者红黑树的结构,会出现数据覆盖等现象
会出现的问题:

  1. 数据丢失
  2. 数据重复
  3. 死循环

2、加载因子0.75,扩容的代价比较大,
需要重新计算hash,旧桶数组中的某个桶的外挂单链表是通过头插法插入新桶数组中的,并且原链表中的Entry结点并不一定仍然在新桶数组的同一链表

参考:
HashMap工作原理和扩容机制

相关:
HashMap在什么场景下会由哪些内部方法导致线程不安全
HashMap并发时的坑
高性能场景下,HashMap的优化使用建议


6、HashMap&ConcurrentHashMap占用CPU100%

6.1、不正确的使用HashMap(JDK7)造成CPU占用100%

线程不安全的HashMap, HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,查找时会陷入死循环,导致CPU占用达到100%。JDK8解决。

参考:
并发的HashMap为什么会引起死循环
并发场景下HashMap死循环导致CPU100%的问题
Java并发容器ConcurrentHashMap原理及HashMap死循环原因的分析

6.2、ConcurrentHashMap(JDK8)造成CPU 100%

为什么会发生这个BUG,答案就在ConcurrentHashMap中的computeIfAbsent方法中。怎么规避这个问题呢?只要不在递归中使用computeIfAbsent方法就好啦,或者降级用分段锁,或者升级JDK(JDK9解决)

参考:
不止JDK7的HashMap,JDK8的ConcurrentHashMap也会造成CPU 100%
原来,JDK8的ConcurrentHashMap也会造成CPU 100%


7、HashSet & LinkedHashSet

1、HashSet的public类型构造函数均是采用HashMap实现,所以HashSet能够存储不重复的对象,包括NULL。
2、LinkedHashSet通过继承HashSet,采用LinkedhashMap进行实现,所以LinkedHashSet除了具有HashSet的功能外,还能保证元素按照加入顺序进行排序。

相关:
搞懂 HashSet & LinkedHashSet 源码
JAVA集合Set之LinkedHashSet详解
jdk1.8中HashSet与LinkedHashSet源码分析


8、JDK9-11新特性

全面了解JDK9的新特性
JDK10新特性
【转载】Java 11 中 11 个不为人知的瑰宝
从JDK8到JDK11,带来了哪些新特性新变化

猜你喜欢

转载自blog.csdn.net/zangdaiyang1991/article/details/89959286